Capacity#
Channel capacity: the reference axis every other metric is read against.
A code is not judged in the abstract but by its distance to capacity, and a modulation not by its SER alone but by the rate it can carry. This module provides the closed forms where they exist (Gaussian input over AWGN, Rayleigh ergodic capacity) and the numerical ones where they do not (constellation-constrained and BICM capacity, MIMO), so that a simulated curve can be placed against the limit it is chasing.
Every function returns bits per channel use.
- awgn_capacity(snr: ndarray | float) ndarray[source]#
Shannon capacity of the complex AWGN channel.
Signal Model#
For \(y = x + b\) with \(b \sim \mathcal{CN}(0, \sigma^2)\) and an average power constraint \(\mathbb{E}\left[|x|^2\right] \leq P\), the capacity is reached by a Gaussian input and equals
\[C = \log_2\left(1 + \rho\right), \qquad \rho = P / \sigma^2\]- param snr:
Signal-to-noise ratio \(\rho\), in linear scale.
- type snr:
float or np.ndarray
- returns:
np.ndarray – Capacity in bits per channel use.
References
C. E. Shannon, “A mathematical theory of communication”, Bell System Technical Journal 27, 1948; T. M. Cover, J. A. Thomas, Elements of Information Theory, 2nd ed., Wiley, 2006, Chapter 9.
Examples
>>> print(np.round(awgn_capacity(np.array([1.0, 10.0, 100.0])), 4)) [1. 3.4594 6.6582]
- constellation_capacity(alphabet: ndarray, snr: ndarray | float, *, px: ndarray | None = None, n_nodes: int = 40, method: str = 'gauss-hermite') ndarray[source]#
Mutual information of a discrete constellation over AWGN.
Signal Model#
A real system does not transmit a Gaussian: it transmits one of \(M\) constellation points, equiprobably by default. The achievable rate is then the mutual information
\[I(X; Y) = \log_2 M - \frac{1}{M} \sum_{m=1}^{M} \mathbb{E}_{b}\left[ \log_2 \sum_{m'=1}^{M} \exp\left(-\frac{|x_m - x_{m'} + b|^2 - |b|^2}{\sigma^2}\right) \right]\]where the expectation is over the noise \(b \sim \mathcal{CN}(0, \sigma^2)\). This is the constrained capacity: it saturates at \(\log_2 M\) however high the SNR, and it is the ceiling a coded system using that constellation can approach – not the Shannon capacity, which assumes a Gaussian input.
Probabilistic shaping makes the input non-uniform, and then the \(1/M\) factors are no longer constants to pull out of the sum:
\[I(X; Y) = - \sum_{m} P_X(x_m) \; \mathbb{E}_{b}\left[ \log_2 \sum_{m'} P_X(x_{m'}) \exp\left(-\frac{|x_m - x_{m'} + b|^2 - |b|^2}{\sigma^2}\right) \right]\]which is the same expression with
pxin both places and reduces to the one above when it is uniform. Passingpxis how the rate of a shaped constellation is read exactly rather than estimated from samples withcompute_mi().The expectation is a two-dimensional integral against a Gaussian weight, evaluated by quadrature – no random draws. Which rule does it is left open by
method; see_noise_quadrature()for what separates them.- param alphabet:
Constellation points, of unit average energy (as returned by
get_alphabet()).- type alphabet:
np.ndarray
- param snr:
Signal-to-noise ratio \(\rho = 1/\sigma^2\), linear.
- type snr:
float or np.ndarray
- param px:
Input distribution \(P_X\), of length \(M\). Default (None) is uniform. Note that the SNR is still read from the alphabet’s own scale, so a shaped input of lower energy is compared at lower power unless the alphabet is rescaled – see
maxwell_boltzmann().- type px:
np.ndarray, optional, keyword-only
- param n_nodes:
Quadrature nodes per noise dimension, so the cost grows as \(n^2\). Default 40.
- type n_nodes:
int, optional, keyword-only
- param method:
"gauss-hermite"(default), the rule matched to the Gaussian weight, or"simpson", the classical composite rule on a truncated grid. Same quantity, different numerics: the second is there to be able to check the first without leaving the library.- type method:
str, optional, keyword-only
- returns:
np.ndarray – Mutual information in bits per channel use, in \([0, \log_2 M]\) – at most \(H(P_X)\) when
pxis given.- raises ValueError:
If
methodis not one of the two rules above, or ifpxdoes not have one non-negative probability per constellation point.
References
G. Ungerboeck, “Channel coding with multilevel/phase signals”, IEEE Trans. Inf. Theory 28(1), 1982; Cover & Thomas, 2nd ed., Chapter 9.
Examples
>>> from comnumpy.core.utils import get_alphabet >>> qpsk = get_alphabet("PSK", 4) >>> print(np.round(constellation_capacity(qpsk, np.array([1.0, 1e4])), 4)) [0.9719 2. ]
The classical rule lands on the same number, which is the point of keeping it – it checks the default rule without leaving the library:
>>> qam16 = get_alphabet("QAM", 16) >>> gauss = constellation_capacity(qam16, 10.0) >>> simpson = constellation_capacity(qam16, 10.0, method="simpson") >>> print(np.round([float(gauss), float(simpson)], 3)) [3.164 3.164]
A non-uniform input carries less than \(\log_2 M\) however clean the channel, because it is its entropy that saturates:
>>> law = np.array([0.4, 0.1, 0.1, 0.4]) >>> print(round(float(constellation_capacity( ... get_alphabet("PSK", 4), 1e6, px=law)), 4)) 1.7219
- bicm_capacity(alphabet: ndarray, snr: ndarray | float, *, px: ndarray | None = None, n_nodes: int = 40, method: str = 'gauss-hermite') ndarray[source]#
BICM capacity: the bound a bit-interleaved coded system chases.
Signal Model#
A chain that maps bits to symbols, demaps to per-bit LLRs and then decodes does not achieve the constellation capacity: the parallel binary channels seen by the decoder ignore the dependence between the bits of a symbol. The achievable rate is the sum of the per-bit mutual informations
\[C_{\mathrm{BICM}} = \sum_{i=1}^{m} I(B_i; Y), \qquad m = \log_2 M\]which is the right reference for the
SymbolDemapper(soft=True)+ FEC chain of this library. The gap toconstellation_capacity()is the price of the bit-wise interface, and it depends on the labelling – Gray mapping minimizes it, which is why it is the default.- param alphabet:
Constellation points of unit average energy, in the labelling order used by the mapper (index = symbol value).
- type alphabet:
np.ndarray
- param snr:
Signal-to-noise ratio, linear.
- type snr:
float or np.ndarray
- param n_nodes:
Quadrature nodes per noise dimension. Default 40.
- type n_nodes:
int, optional, keyword-only
- param method:
"gauss-hermite"(default) or"simpson", as inconstellation_capacity().- type method:
str, optional, keyword-only
- returns:
np.ndarray – BICM capacity in bits per channel use.
- raises ValueError:
If the constellation size is not a power of two, or
methodis not one of the two rules.
References
G. Caire, G. Taricco, E. Biglieri, “Bit-interleaved coded modulation”, IEEE Trans. Inf. Theory 44(3), 1998.
Examples
>>> from comnumpy.core.utils import get_alphabet >>> qam16 = get_alphabet("QAM", 16) >>> snr = np.array([1.0, 10.0]) >>> print(np.round(bicm_capacity(qam16, snr), 4)) # bit-wise interface [0.8993 3.1636] >>> print(np.round(constellation_capacity(qam16, snr), 4)) # the ceiling above it [0.9897 3.1639]
A shaped input is passed the same way as to
constellation_capacity(), and the ceiling moves with it: the sum of the per-bit entropies, not \(\log_2 M\).>>> from comnumpy.core.shaping import maxwell_boltzmann >>> law = maxwell_boltzmann(qam16, lam=0.5) >>> print(np.round(bicm_capacity(qam16, snr, px=law), 4)) [0.7928 3.0653]
- rayleigh_ergodic_capacity(snr: ndarray | float) ndarray[source]#
Ergodic capacity of a flat Rayleigh fading channel.
Signal Model#
With \(y = h x + b\), \(h \sim \mathcal{CN}(0,1)\) known at the receiver only, the ergodic capacity averages the instantaneous one over the fading:
\[C = \mathbb{E}_{h}\left[\log_2\left(1 + \rho |h|^2\right)\right] = \frac{1}{\ln 2} \, e^{1/\rho} \, E_1\!\left(1/\rho\right)\]since \(|h|^2 \sim \mathrm{Exp}(1)\), with \(E_1\) the exponential integral. Fading always costs: this is strictly below
awgn_capacity()at the same \(\rho\), by Jensen’s inequality.- param snr:
Average signal-to-noise ratio \(\rho\), linear.
- type snr:
float or np.ndarray
- returns:
np.ndarray – Ergodic capacity in bits per channel use.
References
D. Tse, P. Viswanath, Fundamentals of Wireless Communication, Cambridge University Press, 2005, Section 5.4.
Examples
>>> capacity = rayleigh_ergodic_capacity(np.array([1.0, 10.0, 100.0])) >>> print(np.round(capacity, 4)) [0.8603 2.9065 5.884 ] >>> bool(np.all(capacity < awgn_capacity(np.array([1.0, 10.0, 100.0])))) True
- mimo_ergodic_capacity(n_tx: int, n_rx: int, snr: ndarray | float, *, n_realizations: int = 2000, rng: Generator | None = None) ndarray[source]#
Ergodic capacity of an i.i.d. Rayleigh MIMO channel.
Signal Model#
With equal power split across the transmit antennas and channel knowledge at the receiver only,
\[C = \mathbb{E}_{\mathbf{H}}\left[ \log_2 \det\left( \mathbf{I}_{N_r} + \frac{\rho}{N_t} \mathbf{H} \mathbf{H}^H \right) \right]\]with \(\mathbf{H}\) of i.i.d. \(\mathcal{CN}(0,1)\) entries. The famous consequence is the multiplexing gain: at high SNR the capacity grows as \(\min(N_t, N_r) \log_2 \rho\), i.e. spatial dimensions buy rate, not just reliability.
No closed form exists for finite dimensions, so the expectation is estimated by Monte Carlo.
- param n_tx:
Numbers of transmit and receive antennas \(N_t\), \(N_r\).
- type n_tx:
int
- param n_rx:
Numbers of transmit and receive antennas \(N_t\), \(N_r\).
- type n_rx:
int
- param snr:
Total signal-to-noise ratio \(\rho\), linear.
- type snr:
float or np.ndarray
- param n_realizations:
Channel draws averaged. Default 2000.
- type n_realizations:
int, optional, keyword-only
- param rng:
Random generator.
- type rng:
numpy.random.Generator, optional, keyword-only
- returns:
np.ndarray – Ergodic capacity in bits per channel use.
References
E. Telatar, “Capacity of multi-antenna Gaussian channels”, European Trans. Telecommunications 10(6), 1999; Tse & Viswanath, Chapter 8.
Examples
>>> rng = np.random.default_rng(0) >>> siso = mimo_ergodic_capacity(1, 1, 10.0, n_realizations=20000, rng=rng) >>> bool(abs(siso - rayleigh_ergodic_capacity(10.0)) < 0.05) # 1x1 = SISO True
- outage_capacity(snr: float, outage: float = 0.01) float[source]#
Outage capacity of a flat Rayleigh channel.
Signal Model#
A slowly fading channel has no ergodic behaviour over one codeword: the rate either is or is not supported. The \(\varepsilon\)-outage capacity is the largest rate supported with probability \(1 - \varepsilon\). With \(|h|^2 \sim \mathrm{Exp}(1)\), \(\mathbb{P}\left[|h|^2 < t\right] = 1 - e^{-t}\), hence
\[C_{\varepsilon} = \log_2\left(1 - \rho \ln\left(1 - \varepsilon\right)\right)\]It is far below the ergodic capacity at small \(\varepsilon\) – the price of not being able to average over the fading.
- param snr:
Average signal-to-noise ratio \(\rho\), linear.
- type snr:
float
- param outage:
Outage probability \(\varepsilon\) in \((0, 1)\). Default 0.01.
- type outage:
float, optional
- returns:
float – Outage capacity in bits per channel use.
References
Tse & Viswanath, Fundamentals of Wireless Communication, 2005, Section 5.4.1.
Examples
>>> print(round(outage_capacity(100.0, outage=0.01), 4)) 1.0036 >>> print(round(rayleigh_ergodic_capacity(100.0).item(), 4)) 5.884
- waterfilling(gains: ndarray, snr: float) tuple[ndarray, float][source]#
Optimal power allocation over parallel Gaussian channels.
Signal Model#
Given \(K\) parallel channels of gain \(g_k\) – the subcarriers of an OFDM system, the eigenmodes of a MIMO channel – the capacity-achieving allocation solves
\[\max_{p_k \geq 0} \sum_k \log_2\left(1 + \rho g_k p_k\right) \quad \text{subject to} \quad \sum_k p_k = K\]whose solution pours power onto the good channels up to a common water level \(\mu\):
\[p_k = \left(\mu - \frac{1}{\rho g_k}\right)^{+}\]Channels below the water level get nothing: it is not worth spending power where the noise dominates.
- param gains:
Channel power gains \(g_k\), non-negative.
- type gains:
np.ndarray
- param snr:
Reference signal-to-noise ratio \(\rho\) at unit power.
- type snr:
float
- returns:
tuple of (np.ndarray, float) – The allocation \(p_k\) (summing to \(K\)) and the resulting capacity in bits per channel use, per channel.
- raises ValueError:
If a gain is negative or all gains are zero.
References
T. M. Cover, J. A. Thomas, Elements of Information Theory, 2nd ed., Wiley, 2006, Section 9.4 (parallel Gaussian channels).
Examples
>>> power, capacity = waterfilling(np.array([1.0, 1.0, 1.0]), snr=10.0) >>> print(np.round(power, 6), round(capacity, 4)) [1. 1. 1.] 3.4594 >>> power, capacity = waterfilling(np.array([4.0, 1.0, 0.01]), snr=10.0) >>> print(np.round(power, 4), round(capacity, 4) + 0.0) [1.5375 1.4625 0. ] 3.3105