Utilities#
- rayleigh_channel(N_r: int, N_t: int, L: int = 1, scale_per_tap: Sequence[float] | None = None, seed: int | None = None, rng: Generator | None = None, size: int | None = None) ndarray[source]#
Draw an i.i.d. Rayleigh MIMO channel.
Signal Model#
The channel taps feed the MIMO observation model
\[\mathbf{y}[n] = \sum_{l=0}^{L-1} \mathbf{H}[l]\, \mathbf{x}[n-l] + \mathbf{b}[n]\]Each coefficient of the \(N_r \times N_t\) matrix \(\mathbf{H}[l]\) is drawn independently from a circularly symmetric complex normal law
\[H_{ij}[l] \sim \mathcal{CN}\left(0, \gamma_l\right), \qquad \gamma_l = \texttt{scale\_per\_tap}[l]\]i.e. real and imaginary parts are independent \(\mathcal{N}(0, \gamma_l/2)\), so that \(\mathbb{E}\left[|H_{ij}[l]|^2\right] = \gamma_l\). The modulus \(|H_{ij}[l]|\) is then Rayleigh-distributed and the instantaneous power is exponential – the classical model for a rich scattering environment with no dominant path.
Normalization: with the default \(\gamma_l = 1\) every antenna pair has unit average gain (no \(1/\sqrt{N_t}\) transmit normalization is applied, hence \(\mathbb{E}\|\mathbf{H}[l]\|_F^2 = N_r N_t\)), and the total multipath energy is \(L\). Feeding
pdp_to_scales()gives instead \(\sum_l \gamma_l = 1\), i.e. a delay spread that does not change the received power.Layout: the returned array follows
(L, N_r, N_t)– taps first, then the MIMO layout of CONVENTIONS.md (receive antennas on axis -2) – and is squeezed to(N_r, N_t)whenL == 1, which is exactly whatFlatMIMOChannelandSelectiveMIMOChannelexpect.- param N_r:
Number of receive antennas \(N_r\).
- type N_r:
int
- param N_t:
Number of transmit antennas \(N_t\).
- type N_t:
int
- param L:
Number of discrete-time channel taps \(L\). Default is 1 (flat channel).
- type L:
int, optional
- param scale_per_tap:
Per-tap variances \(\gamma_l\) in linear scale (length \(L\)). If None, uses ones.
- type scale_per_tap:
sequence of float, optional
- param seed:
Random seed used if rng is not provided.
- type seed:
int, optional
- param rng:
Pre-initialized random number generator. If provided, seed is ignored.
- type rng:
numpy.random.Generator, optional
- param size:
Number of independent channel draws. When given, the returned array grows a leading axis of length
size– a stack of channels, one per Monte-Carlo trial, drawn sequentially from the same generator so draw \(k\) equals the \(k\)-th draw of the equivalent Python loop. The stack is whatFlatMIMOChanneland the detectors accept as a batchedH(D51).- type size:
int, optional
- returns:
numpy.ndarray – Array of shape (L, N_r, N_t) containing the Rayleigh channel taps – squeezed to (N_r, N_t) when
L == 1, with a leadingsizeaxis whensizeis given.- raises AssertionError:
If scale_per_tap is provided and its length is not equal to L.
References
D. Tse and P. Viswanath, Fundamentals of Wireless Communication, Cambridge University Press, 2005, Chapter 2 (Rayleigh fading) and Chapter 7 (MIMO channel models).
A. Goldsmith, Wireless Communications, Cambridge University Press, 2005, Chapter 3.
Examples
>>> H = rayleigh_channel(N_r=2, N_t=2, L=3, seed=0) >>> H.shape (3, 2, 2) >>> H.dtype dtype('complex128') >>> H1 = rayleigh_channel(N_r=4, N_t=2, seed=0) >>> H1.shape # with the default L=1, the tap axis is squeezed (4, 2) >>> H2 = rayleigh_channel(N_r=32, N_t=32, seed=1) >>> print(round(float(np.mean(np.abs(H2)**2)), 2)) # unit average gain 1.01 >>> stack = rayleigh_channel(N_r=2, N_t=2, seed=0, size=200) >>> stack.shape # one draw per Monte-Carlo trial (200, 2, 2)
- rician_channel(N_r: int, N_t: int, K: float, L: int = 1, H_los: ndarray | None = None, scale_per_tap: Sequence[float] | None = None, seed: int | None = None, rng: Generator | None = None) ndarray[source]#
Draw a Rician MIMO channel with an optional line-of-sight (LoS) component.
Signal Model#
A Rician channel splits each tap into a deterministic (specular) part and a diffuse part:
\[\mathbf{H}[l] = \sqrt{\frac{K}{K+1}}\, \mathbf{H}_{\mathrm{los}}[l] + \sqrt{\frac{1}{K+1}}\, \mathbf{W}[l], \qquad W_{ij}[l] \sim \mathcal{CN}\left(0, \gamma_l\right)\]with \(\gamma_l =\)
scale_per_tap[l]. The Rician factor \(K\) is the ratio of the specular power to the diffuse power; the weights \(\sqrt{K/(K+1)}\) and \(\sqrt{1/(K+1)}\) are chosen so that the total power is preserved: if the entries of \(\mathbf{H}_{\mathrm{los}}[l]\) have unit modulus and \(\gamma_l = 1\), then \(\mathbb{E}\left[|H_{ij}[l]|^2\right] = K/(K+1) + 1/(K+1) = 1\) for every \(K\). Two limits: \(K = 0\) gives back the Rayleigh channel ofrayleigh_channel(), and \(K \to \infty\) gives the deterministic LoS channel. The modulus of each coefficient is then Rice-distributed.Layout: the returned array follows
(L, N_r, N_t), squeezed to(N_r, N_t)whenL == 1.- param N_r:
Number of receive antennas \(N_r\).
- type N_r:
int
- param N_t:
Number of transmit antennas \(N_t\).
- type N_t:
int
- param K:
Rician K-factor \(K\) (specular-to-diffuse power ratio, linear scale).
- type K:
float
- param L:
Number of discrete-time channel taps \(L\). Default is 1.
- type L:
int, optional
- param H_los:
Deterministic LoS component \(\mathbf{H}_{\mathrm{los}}\) of shape (L, N_r, N_t) or (N_r, N_t). If None, a zero LoS is assumed. Must be complex-valued (see Notes).
- type H_los:
numpy.ndarray, optional
- param scale_per_tap:
Per-tap variances \(\gamma_l\) of the diffuse component (length \(L\)). If None, uses ones.
- type scale_per_tap:
sequence of float, optional
- param seed:
Random seed used if rng is not provided.
- type seed:
int, optional
- param rng:
Pre-initialized random number generator. If provided, seed is ignored.
- type rng:
numpy.random.Generator, optional
- returns:
numpy.ndarray – Array of shape (L, N_r, N_t) containing the Rician channel taps.
Notes
K = 0 reduces to a Rayleigh channel with variance scale_per_tap[l].
As K → ∞, the channel approaches the deterministic LoS component H_los.
If H_los is provided as (N_r, N_t), it is broadcast to (1, N_r, N_t).
The output array inherits the dtype of H_los: a real-valued H_los silently discards the imaginary part of the diffuse component (a
ComplexWarningis emitted). Passnp.ones((N_r, N_t), dtype=complex)rather thannp.ones((N_r, N_t)).
- raises AssertionError:
If H_los (after normalization) does not match (L, N_r, N_t), or if scale_per_tap is provided and its length is not equal to L.
References
A. Goldsmith, Wireless Communications, Cambridge University Press, 2005, Chapter 3 (Rician fading and the K-factor).
D. Tse and P. Viswanath, Fundamentals of Wireless Communication, Cambridge University Press, 2005, Chapter 2.
Examples
>>> H = rician_channel(N_r=2, N_t=2, K=6.0, seed=0) >>> H.shape # with the default L=1, the tap axis is squeezed (2, 2) >>> H.dtype dtype('complex128') >>> H_los = np.ones((2, 2), dtype=complex) # unit-modulus LoS >>> print(np.round(rician_channel(N_r=2, N_t=2, K=9.0, H_los=H_los, seed=0), 4)) [[0.9768-0.1198j 0.9191+0.0809j] [1.0919+0.2916j 0.9721+0.2118j]]
- kronecker_rayleigh_channel(N_r: int, N_t: int, L: int = 1, R_rx: ndarray | None = None, R_tx: ndarray | None = None, scale_per_tap: Sequence[float] | None = None, seed: int | None = None, rng: Generator | None = None) ndarray[source]#
Draw a spatially correlated Rayleigh MIMO channel (Kronecker model).
Signal Model#
The Kronecker (separable) model assumes that the correlation seen at the transmitter is independent of the one seen at the receiver, so each tap factorizes as
\[\mathbf{H}[l] = \mathbf{R}_r^{1/2}\, \mathbf{W}[l]\, \left(\mathbf{R}_t^{1/2}\right)^{T}, \qquad W_{ij}[l] \sim \mathcal{CN}\left(0, \gamma_l\right)\]where \(\gamma_l =\)
scale_per_tap[l]and the square roots are the lower-triangular Cholesky factors \(\mathbf{R}_r = \mathbf{L}_r\mathbf{L}_r^H\), \(\mathbf{R}_t = \mathbf{L}_t\mathbf{L}_t^H\). Stacking the columns of a tap then gives the separable covariance that defines the model:\[\mathbb{E}\left[\mathrm{vec}(\mathbf{H}[l])\, \mathrm{vec}(\mathbf{H}[l])^H\right] = \gamma_l \, \mathbf{R}_t \otimes \mathbf{R}_r\]Setting a correlation matrix to None amounts to using the identity on that side; with both set to None the i.i.d. channel of
rayleigh_channel()is recovered. Correlation does not change the average power of a coefficient when the correlation matrices have unit diagonal, but it lowers the rank of a typical realization – which is exactly what costs spatial multiplexing gain.Layout: the returned array follows
(L, N_r, N_t), squeezed to(N_r, N_t)whenL == 1.- param N_r:
Number of receive antennas \(N_r\).
- type N_r:
int
- param N_t:
Number of transmit antennas \(N_t\).
- type N_t:
int
- param L:
Number of discrete-time channel taps \(L\). Default is 1.
- type L:
int, optional
- param R_rx:
Receive-side correlation matrix \(\mathbf{R}_r\) of shape (N_r, N_r). If None, identity is used.
- type R_rx:
numpy.ndarray, optional
- param R_tx:
Transmit-side correlation matrix \(\mathbf{R}_t\) of shape (N_t, N_t). If None, identity is used.
- type R_tx:
numpy.ndarray, optional
- param scale_per_tap:
Per-tap variances \(\gamma_l\) in linear scale (length \(L\)). If None, uses ones.
- type scale_per_tap:
sequence of float, optional
- param seed:
Random seed used if rng is not provided.
- type seed:
int, optional
- param rng:
Pre-initialized random number generator. If provided, seed is ignored.
- type rng:
numpy.random.Generator, optional
- returns:
numpy.ndarray – Array of shape (L, N_r, N_t) containing the correlated Rayleigh channel taps.
Notes
Both correlation matrices must be Hermitian positive definite, as required by
numpy.linalg.cholesky. The right factor is transposed, not conjugate-transposed: this is what makes the transmit-side factor of the covariance \(\mathbf{R}_t\) itself (and not its conjugate) with the column-stackingvecconvention.- raises AssertionError:
If scale_per_tap is provided and its length is not equal to L.
References
J. P. Kermoal, L. Schumacher, K. I. Pedersen, P. E. Mogensen and F. Frederiksen, “A stochastic MIMO radio channel model with experimental validation,” IEEE J. Sel. Areas Commun., vol. 20, no. 6, pp. 1211-1226, 2002.
D. Tse and P. Viswanath, Fundamentals of Wireless Communication, Cambridge University Press, 2005, Chapter 7.
Examples
>>> Rr = np.array([[1.0, 0.5], [0.5, 1.0]]) >>> Rt = np.eye(2) >>> H = kronecker_rayleigh_channel(N_r=2, N_t=2, L=2, R_rx=Rr, R_tx=Rt, seed=0) >>> H.shape (2, 2, 2) >>> H.dtype dtype('complex128')
- pdp_to_scales(pdp_db: Sequence[float]) ndarray[source]#
Convert a power delay profile (PDP) in dB into normalized per-tap variances.
Signal Model#
A power delay profile gives the average received power \(P_l\) (in dB) of the path arriving with delay \(l T_s\). It is converted to the linear per-tap variances \(\gamma_l = \mathbb{E}\left[|H_{ij}[l]|^2\right]\) used by the channel generators, and normalized to unit total energy:
\[\gamma_l = \frac{10^{P_l/10}} {\displaystyle\sum_{j=0}^{L-1} 10^{P_j/10}}, \qquad \sum_{l=0}^{L-1} \gamma_l = 1\]The normalization is what makes a comparison across delay profiles meaningful: the delay spread changes the frequency selectivity of the channel without changing the total received power, hence without changing the operating SNR.
Layout: 1-D array of length \(L\), to be passed as
scale_per_taptorayleigh_channel(),rician_channel()orkronecker_rayleigh_channel().- param pdp_db:
Per-tap powers \(P_l\) in dB (length \(L\)), the first entry being the first arriving path.
- type pdp_db:
sequence of float
- returns:
numpy.ndarray – One-dimensional array of length L with non-negative entries summing to 1.
Notes
If the PDP contains all -inf values or sums to zero after conversion, the result would be undefined; ensure the PDP has finite values.
References
A. Goldsmith, Wireless Communications, Cambridge University Press, 2005, Chapter 3 (multipath channel, power delay profile and delay spread).
Examples
>>> scales = pdp_to_scales([0.0, -3.0, -6.0]) >>> print(np.round(scales, 4)) [0.5707 0.286 0.1433] >>> print(float(round(scales.sum(), 6))) 1.0