Channels#
- class AWGN(*, snr_dB: float | None = None, sigma2: float | None = None, seed: int | None = None, name: str = 'awgn')[source]#
Additive white Gaussian noise channel.
Signal Model#
\[y[n] = x[n] + b[n], \qquad b[n] \sim \mathcal{CN}\left(0, \sigma^2\right)\]When parameterized by
snr_dB, the variance is derived from the measured input power \(P_x = \mathbb{E}\left[|x[n]|^2\right]\):\[\sigma^2 = P_x \, 10^{-\mathrm{SNR_{dB}}/10}\]For real-valued inputs, a real Gaussian noise of variance \(\sigma^2\) is applied instead of a circular complex one.
Axes: element-wise – applied pointwise, shape-agnostic. One caveat under a batch (D51): with
snr_dB=the signal power is measured over the whole input, batch rows included, so rows of unequal power share one variance rather than each receiving the requested SNR;sigma2=is batch-neutral.- param snr_dB:
Signal-to-noise ratio \(\mathrm{SNR_{dB}}\) in decibels, relative to the measured input power. Mutually exclusive with
sigma2.- type snr_dB:
float, keyword-only
- param sigma2:
Absolute noise variance \(\sigma^2\). Mutually exclusive with
snr_dB.- type sigma2:
float, keyword-only
- param seed:
Local RNG seed.
- type seed:
int, optional, keyword-only
- param name:
Name of the channel instance. Default is
"awgn".- type name:
str, optional, keyword-only
- sigma2_#
Variance actually applied. Estimated from the input power when parameterized by
snr_dB(data-dependent, hence the trailing underscore); equal tosigma2otherwise.- Type:
float
References
J. G. Proakis, M. Salehi, Digital Communications, 5th ed., McGraw-Hill, 2008, Section 4.2.
Examples
>>> x = np.zeros(10_000, dtype=complex) >>> y = AWGN(sigma2=0.01, seed=42)(x) >>> print(round(float(np.var(y)), 3)) 0.01
- class FIRChannel(h: ndarray, mode: Literal['full', 'same', 'valid'] = 'full', name: str = 'fir')[source]#
Finite impulse response (FIR) channel with a given impulse response.
Signal Model#
The output \(y[n]\) is the convolution of the input \(x[n]\) with the impulse response \(h[l]\) of length \(L\):
\[y[n] = \sum_{l=0}^{L-1} h[l] \, x[n-l]\]Axes: axis -1 – the convolution runs along the last axis (
scipy.signal.convolvesemantics; output length depends onmode); leading axes are batch, every row through the same taps.- param h:
The impulse response \(h[l]\) of the channel, a 1D array of length \(L\).
- type h:
np.ndarray
- param mode:
Convolution mode passed to
scipy.signal.convolve. Default is"full"(output length \(N + L - 1\)).- type mode:
Literal[“full”, “same”, “valid”], optional
- param name:
The name of the channel instance. Default is
"fir".- type name:
str, optional
References
J. G. Proakis, M. Salehi, Digital Communications, 5th ed., McGraw-Hill, 2008, Section 9.3 (channels with ISI).
Examples
>>> channel = FIRChannel(np.array([1.0, 0.5])) >>> print(channel(np.array([1.0, 0.0, 2.0]))) [1. 0.5 2. 1. ]
- info() dict[str, Any][source]#
What this channel is, as a dictionary.
The figures an engineer reads off a tap vector before deciding anything: how long it is, how much of the energy sits in the first path, and how far the frequency response swings between its best and its worst point – the number that says whether one equalizer tap will do.
- Returns:
dict of str to object
Examples
>>> info = FIRChannel(np.array([1.0, 0.0, 0.5])).info() >>> info["n_taps"], round(info["peak_to_notch_dB"], 2) (3, 9.54)
- class TappedDelayLineChannel(profile: PowerDelayProfile, *, fs: float, f_doppler: float = 0.0, seed: int | None = None, name: str = 'fading')[source]#
Time-varying multipath fading channel (decision D43).
Signal Model#
The received signal is the sum of the \(L\) resolvable paths of a
PowerDelayProfile, each weighted by its own fading process:\[y[n] = \sum_{l=0}^{L-1} a_l[n] \, x[n - d_l], \qquad \mathbb{E}\left[|a_l[n]|^2\right] = \gamma_l\]where \(d_l = \mathrm{round}(\tau_l f_s)\) is the path delay in samples and \(\gamma_l\) its normalized power. Each \(a_l[n]\) is an independent Rayleigh process with the profile’s Doppler spectrum (
rayleigh_process()), so the channel varies within a single call: this is a time-selective as well as frequency-selective model, not block fading. Settingf_doppler=0recovers block fading, one draw per call.When the profile declares a Rice factor \(K\), the first path carries a specular component:
\[a_0[n] = \sqrt{\gamma_0}\left(\sqrt{\frac{K}{K+1}} \, e^{j \phi} + \sqrt{\frac{1}{K+1}} \, g[n]\right)\]where \(g[n]\) is the unit-power Rayleigh process of the first path, so that \(\mathbb{E}[|a_0[n]|^2] = \gamma_0\) still holds.
The output keeps the input length: the delayed copies are zero-padded at the start, i.e. the channel is causal and the transient is kept.
Axes: axis -1 – expects a 1D serial signal
(N,).- param profile:
Delay profile, from the catalog or hand-built.
- type profile:
PowerDelayProfile
- param fs:
Sampling frequency \(f_s\) in Hz; sets the mapping from the profile’s delays in ns onto the sample grid.
- type fs:
float, keyword-only
- param f_doppler:
Maximum Doppler \(f_D\) in Hz. Default 0.0 (block fading).
- type f_doppler:
float, optional, keyword-only
- param seed:
Local RNG seed; overridden by chain seeding (
Sequential.seed).- type seed:
int, optional, keyword-only
- param name:
Name of the channel instance. Default
"fading".- type name:
str, optional, keyword-only
- h_#
Realized time-varying impulse response of the last run, shape
(L, N)– data-dependent, hence the trailing underscore (D23). Rowlis \(a_l[n]\); the matching delays aredelays_.- Type:
np.ndarray
- delays_#
Path delays in samples used for the last run.
- Type:
np.ndarray
- raises ShapeError:
If the input is not 1D, or is shorter than the profile’s longest path at this sampling rate.
References
J. G. Proakis, M. Salehi, Digital Communications, 5th ed., McGraw-Hill, 2008, Section 13.1; W. C. Jakes, Microwave Mobile Communications, Wiley, 1974, Chapter 1.
Examples
>>> from comnumpy.core.fading import get_delay_profile >>> channel = TappedDelayLineChannel(get_delay_profile("EVA"), ... fs=15.36e6, seed=1) >>> y = channel(np.ones(1000, dtype=complex)) >>> y.shape, channel.delays_.tolist()[:4] ((1000,), [0, 2, 5, 6]) >>> channel.h_.shape # one fading process per resolvable path (8, 1000)
- impulse_response(n_samples: int | None = None) ndarray[source]#
Draw one realization and return it as a tap vector.
Signal Model#
Sounding a channel with an impulse is how its response is measured, and it is what a script needs whenever a known channel is wanted: the equalizers of
LinearEqualizerandFrequencyDomainEqualizertake a tap vector, not a fading process. This does exactly that – feeds \(\delta[n]\) throughforward()and returns the first \(L\) samples, with \(L\) one more than the longest path in samples.The result is dense: paths that fall between the resolvable delays of this sampling rate appear as zeros, so the vector can be handed straight to a convolution.
This draws a new realization and advances the generator, the way calling the block does. Seed it (or the chain) to get the same channel twice.
Axes: returns 1D
(L,).- param n_samples:
Length of the sounding record. Defaults to eight times the longest path, which is enough for the profile to fit and for a Doppler process to be sampled meaningfully. Only the first \(L\) samples are returned either way.
- type n_samples:
int, optional
- returns:
np.ndarray – Complex tap vector of length
delays_[-1] + 1.
Examples
>>> from comnumpy.core.fading import get_delay_profile >>> channel = TappedDelayLineChannel(get_delay_profile("EPA"), ... fs=7.68e6, seed=18) >>> h = channel.impulse_response() >>> h.shape # 410 ns of spread at 7.68 MHz (4,) >>> bool(np.all(h[channel.delays_] == channel.h_[:, 0])) True
- info() dict[str, Any][source]#
What this channel is, as a dictionary.
Everything the profile fixes, plus what this sampling rate makes of it: a delay spread in nanoseconds becomes a number of taps, and a coherence bandwidth is what decides whether a subcarrier sees a flat channel.
- Returns:
dict of str to object
Examples
>>> from comnumpy.core.fading import get_delay_profile >>> info = TappedDelayLineChannel(get_delay_profile("EPA"), ... fs=7.68e6).info() >>> info["standard"], info["n_paths"], info["n_taps"] ('EPA', 7, 4) >>> round(info["rms_delay_spread_ns"]), round(info["fs_Hz"] / 1e6, 2) (43, 7.68)
- plot(domain: Literal['impulse', 'frequency'] = 'impulse', *, scale: Literal['linear', 'dB'] = 'linear', ax: Axes | None = None)[source]#
Draw one realization, in delay or in frequency.
A fading channel has no single impulse response, so this draws the last one realized if the block has been run, and a fresh one otherwise – which advances the generator, exactly as
impulse_response()does.See
plot_channel_response().