Profiling a Communication Chain#
In this tutorial, you will learn how to profile a communication chain using comnumpy. Profiling measures the computational cost of each processor, helping you identify performance bottlenecks in complex simulations.
You will build an OFDM communication chain with channel effects, run the simulation, and visualize the profiling results.
What you’ll learn:
How to build an OFDM communication chain with channel effects.
How to use
plot_chain_profilingto measure per-processor execution time.How to identify computational bottlenecks in a simulation chain.
Introduction#
Import Libraries#
We start by importing the necessary libraries:
import numpy as np
import matplotlib.pyplot as plt
from comnumpy.core import Sequential
from comnumpy.core.generators import SymbolGenerator
from comnumpy.core.mappers import SymbolMapper, SymbolDemapper
from comnumpy.core.channels import (AWGN, FIRChannel,
TappedDelayLineChannel)
from comnumpy.core.fading import get_delay_profile
from comnumpy.core.processors import Serial2Parallel, Parallel2Serial
from comnumpy.core.utils import Constellation
from comnumpy.core.visualizers import plot_chain_profiling
from comnumpy.ofdm.processors import CarrierAllocator, FFTProcessor, IFFTProcessor, CyclicPrefixer, CyclicPrefixRemover, CarrierExtractor
from comnumpy.ofdm.compensators import FrequencyDomainEqualizer
from comnumpy.ofdm.allocation import get_allocation
from comnumpy import style
style.use()
Define Parameters#
Next, we define the communication and channel parameters:
fs = 5e6 # Sample rate the channel is resolved at
N_cp = 10 # Cyclic prefix length
N = 100000 # Number of symbols
sigma2 = 0.01 # Noise variance
constellation = Constellation("QAM", 16)
allocation = get_allocation("802.11ac-40") # 128 subcarriers, from the catalog
N_carriers = allocation.N_fft
N_carrier_data = allocation.N_data # Number of data carriers
N_carrier_pilots = allocation.N_pilots # Number of pilot carriers
channel_model = TappedDelayLineChannel(get_delay_profile("TDL-D"), fs=fs,
seed=5)
h = channel_model.impulse_response()
pilots = 10 * np.ones(N_carrier_pilots) # Pilot values
OFDM Communication Chain#
Define the Chain#
We build a complete OFDM chain using the Sequential object.
This chain includes mapping, carrier allocation, IFFT/FFT processing, cyclic prefix handling, channel effects, equalization, and demapping.
chain = Sequential([
SymbolGenerator(constellation.order),
SymbolMapper(constellation),
Serial2Parallel(N_carrier_data),
CarrierAllocator(carrier_type=allocation, pilots=pilots),
IFFTProcessor(),
CyclicPrefixer(N_cp),
Parallel2Serial(),
FIRChannel(h),
AWGN(sigma2=sigma2),
Serial2Parallel(N_carriers + N_cp),
CyclicPrefixRemover(N_cp),
FFTProcessor(),
FrequencyDomainEqualizer(h=h),
CarrierExtractor(allocation),
Parallel2Serial(),
SymbolDemapper(constellation)
])
The chain is composed of the following processors:
SymbolGeneratorGenerates a sequence of integer-valued symbols to transmit.SymbolMapperMaps integers to QAM constellation points.Serial2Parallel/Parallel2SerialReshape data between serial and parallel streams, as required by OFDM processing.CarrierAllocatorAssigns data and pilot symbols to their designated subcarriers.IFFTProcessor/FFTProcessorPerform the Inverse Fast Fourier Transform and Fast Fourier Transform operations, respectively.CyclicPrefixer/CyclicPrefixRemoverAdd and remove the cyclic prefix to prevent inter-symbol interference.FIRChannelModels a frequency-selective multipath channel.AWGNAdds white Gaussian noise.FrequencyDomainEqualizerCompensates for channel distortion in the frequency domain.CarrierExtractorExtracts data and pilot carriers after equalization.SymbolDemapperMaps received constellation points back to integer symbols.
The chain, as the chain itself describes it:
flowchart LR
generator["SymbolGenerator"]
symbol_mapper["SymbolMapper"]
s2_p["Serial2Parallel"]
carrier_allocator["CarrierAllocator"]
ifft["IFFTProcessor"]
cp_adder["CyclicPrefixer"]
p2_s["Parallel2Serial"]
fir["FIRChannel"]
awgn["AWGN"]
s2_p_2["Serial2Parallel"]
cp_remover["CyclicPrefixRemover"]
fft["FFTProcessor"]
frequency_domain_equalizer["FrequencyDomainEqualizer"]
carrier_extractor["CarrierExtractor"]
p2_s_2["Parallel2Serial"]
symbol_demapper["SymbolDemapper"]
generator --> symbol_mapper
symbol_mapper --> s2_p
s2_p --> carrier_allocator
carrier_allocator --> ifft
ifft --> cp_adder
cp_adder --> p2_s
p2_s --> fir
fir --> awgn
awgn --> s2_p_2
s2_p_2 --> cp_remover
cp_remover --> fft
fft --> frequency_domain_equalizer
frequency_domain_equalizer --> carrier_extractor
carrier_extractor --> p2_s_2
p2_s_2 --> symbol_demapper
The diagram above is not drawn by hand. It is what the chain says about
itself – chain.to_mermaid() (decision D33c) – exported by the
script, so the block names are the ones the code uses and a dashed
outline marks an observed block:
mermaid_dir = "../../docs/getting_started/mermaid/"
for diagram_name, diagram_chain in [("profiling_chain", chain)]:
with open(f"{mermaid_dir}/{diagram_name}.mmd", "w") as stream:
stream.write(diagram_chain.to_mermaid())
Profiling the Chain#
To profile the chain, we use the plot_chain_profiling function.
This function measures the execution time of each processor for a given input size
and produces a bar chart of the results.
plot_chain_profiling(chain, input=N)
plt.savefig("../../docs/getting_started/img/profiling_chain_fig1.png")
The resulting figure shows the time spent in each processor, making it easy to identify which stages dominate the computation.
The time axis is logarithmic, and it has to be: a chain spans several decades. Here the cyclic-prefix remover costs a few microseconds – it is a slice – while the symbol demapper costs some twenty milliseconds, four decades more, because it computes the distance from every sample to every constellation point. On a linear axis that single block would fill the figure and every other one would be a line against zero.
Two things follow from reading it. Optimizing anything but the demapper here would be wasted work, and the two FFT-based blocks – which are the ones people expect to be expensive – cost about a millisecond for 100 000 symbols.
Note
plot_chain_profiling repeats the chain N_test times and shows the
distribution, which is why the boxes have whiskers and outliers: a single
timing on a shared machine is not a measurement. For a one-shot table of
what each block hands to the next, with its shape, its dtype and its time,
use summary() instead – it is the
textual counterpart of this figure.
Conclusion#
You have successfully profiled an OFDM communication chain with comnumpy.
Profiling is a powerful tool to:
Detect computational bottlenecks in complex simulations.
Compare the efficiency of different processors or chain configurations.
Optimize large-scale communication scenarios.
From here, you may want to explore:
Profiling different modulation schemes or OFDM sizes.
Comparing different equalization techniques.
Combining profiling with performance metrics such as SER or BER.