Skip to main content

comnum_fec/
bounds.rs

1//! Information-theoretic limits of binary codes on the AWGN channel.
2//!
3//! With BPSK on real AWGN of variance $\sigma^{2}$ per dimension, the LLR of a
4//! transmitted 0 is $L \sim \mathcal{N}(2/\sigma^{2}, 4/\sigma^{2})$, and
5//!
6//! $$
7//! C = 1 - \mathbb{E}\left[\log_{2}(1 + e^{-L})\right], \qquad
8//! V = \operatorname{Var}\left[1 - \log_{2}(1 + e^{-L})\right]
9//! $$
10//!
11//! are the capacity and the dispersion of the binary-input AWGN channel (bits per
12//! channel use; [`biawgn_capacity`], [`biawgn_dispersion`]), both by Gauss–Hermite
13//! quadrature. At a finite length $n$ and $k$ information bits, the normal approximation
14//! of Polyanskiy, Poor & Verdú (2010) gives the smallest achievable block error
15//! probability,
16//!
17//! $$
18//! \varepsilon \approx Q\left(\frac{n C - k + \tfrac{1}{2}\log_{2} n}{\sqrt{n V}}\right)
19//! $$
20//!
21//! ([`normal_approximation`]), and [`shannon_limit_ebn0_db`] the $E_{b}/N_{0}$ at which a
22//! rate-$R$ code can work at all ($n \to \infty$). With $E_{b}/N_{0}$ and the rate
23//! $R = k/n$: $\sigma^{2} = 1 / (2 R E_{b}/N_{0})$. Without the binary constraint, the
24//! limit at the spectral efficiency $\eta = 2R$ is $(2^{\eta} - 1)/\eta$
25//! ([`gaussian_limit_ebn0_db`]).
26//!
27//! [`NormalApproximation`] is the same approximation for any channel of capacity $C$ and
28//! dispersion $V$, $\log_{2} M^{*}(n, \varepsilon) \approx n C - \sqrt{n V}\, Q^{-1}(\varepsilon) +
29//! \frac{1}{2} \log_{2} n$, with the channels of Polyanskiy, Poor & Verdú: the BSC,
30//! $C = 1 - h(\delta)$ and $V = \delta(1 - \delta) \log_{2}^{2}\frac{1 - \delta}{\delta}$; the BEC,
31//! $C = 1 - \delta$ and $V = \delta(1 - \delta)$, without the logarithmic term; the AWGN
32//! channel of SNR $P$ with Gaussian codebooks, $C = \frac{1}{2}\log_{2}(1 + P)$ and
33//! $V = \frac{P (P + 2)}{2 (P + 1)^{2}} \log_{2}^{2} e$ (their eqs. (184), (190), (296)) — and
34//! the binary-input AWGN channel above.
35//!
36//! # Examples
37//! ```
38//! use comnum_fec::bounds::{shannon_limit_ebn0_db, normal_approximation};
39//! // rate 1/2 BPSK: the Shannon limit is 0.187 dB
40//! assert!((shannon_limit_ebn0_db(0.5) - 0.187).abs() < 0.01);
41//! // a (128, 64) code cannot do much better than a block error rate of 1e-3 at 2.4 dB
42//! let p = normal_approximation(128, 64, 2.4);
43//! assert!(p > 5e-4 && p < 3e-3);
44//! ```
45//!
46//! # References
47//!
48//! 1. C. E. Shannon, "A mathematical theory of communication," *Bell Syst. Tech. J.*,
49//!    27, 379–423 and 623–656, 1948.
50//! 2. Y. Polyanskiy, H. V. Poor and S. Verdú, "Channel coding rate in the finite
51//!    blocklength regime," *IEEE Trans. Inf. Theory*, 56(5), 2307–2359, 2010.
52//! 3. G. Liva, L. Gaudio, T. Ninacs and T. Jerkovits, "Code design for short blocks: a
53//!    survey," *Proc. EuCNC*, 2016.
54
55use comnum_math::{db2lin, gaussian_nodes, ln_1p_exp, qfunc};
56
57/// The noise variance per dimension of BPSK at `ebn0_db` and the rate `rate`.
58pub fn bpsk_noise_variance(ebn0_db: f64, rate: f64) -> f64 {
59    1.0 / (2.0 * rate * db2lin(ebn0_db))
60}
61
62/// $1 - \log_{2}(1 + e^{-L})$ without overflow.
63fn info_density(l: f64) -> f64 {
64    1.0 - ln_1p_exp(-l) / std::f64::consts::LN_2
65}
66
67/// The first two moments of the information density at the noise variance `noise_var`:
68/// the channel LLR of a 0 is $L \sim \mathcal{N}(\mu, 2\mu)$, $\mu = 2/\sigma^{2}$, and
69/// $E f(L)$ is the 64-point Gauss–Hermite rule of [`gaussian_nodes`].
70fn moments(noise_var: f64) -> (f64, f64) {
71    let mu = 2.0 / noise_var;
72    let mut m1 = 0.0;
73    let mut m2 = 0.0;
74    for (w, a) in gaussian_nodes(2.0 * mu, false, 64) {
75        let i = info_density(mu + w.re);
76        m1 += a * i;
77        m2 += a * i * i;
78    }
79    (m1, m2)
80}
81
82/// Capacity of the binary-input AWGN channel (bits per channel use) at the noise
83/// variance `noise_var` per dimension.
84pub fn biawgn_capacity(noise_var: f64) -> f64 {
85    moments(noise_var).0
86}
87
88/// Dispersion of the binary-input AWGN channel (bits² per channel use).
89pub fn biawgn_dispersion(noise_var: f64) -> f64 {
90    let (m1, m2) = moments(noise_var);
91    (m2 - m1 * m1).max(0.0)
92}
93
94/// The normal approximation of the smallest block error probability of an $(n, k)$
95/// binary code at `ebn0_db` (Polyanskiy, Poor & Verdú 2010).
96pub fn normal_approximation(n: usize, k: usize, ebn0_db: f64) -> f64 {
97    let s2 = bpsk_noise_variance(ebn0_db, k as f64 / n as f64);
98    NormalApproximation::biawgn(s2).block_error(n, k)
99}
100
101/// The normal approximation of the coding rate at finite blocklength of a channel of
102/// capacity `capacity` (bits per channel use) and dispersion `dispersion` (bits² per
103/// channel use), with the term $\frac{1}{2}\log_{2} n$ when `half_log` (see the module).
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct NormalApproximation {
106    /// The capacity $C$ (bits per channel use).
107    pub capacity: f64,
108    /// The dispersion $V$ (bits² per channel use).
109    pub dispersion: f64,
110    /// Whether the term $\frac{1}{2}\log_{2} n$ applies (all but the BEC).
111    pub half_log: bool,
112}
113
114impl NormalApproximation {
115    /// The binary symmetric channel of crossover probability `delta`.
116    pub fn bsc(delta: f64) -> NormalApproximation {
117        let h = comnum_math::entropy(&[delta, 1.0 - delta]);
118        let v = delta * (1.0 - delta) * ((1.0 - delta) / delta).log2().powi(2);
119        NormalApproximation { capacity: 1.0 - h, dispersion: v, half_log: true }
120    }
121
122    /// The binary erasure channel of erasure probability `delta`.
123    pub fn bec(delta: f64) -> NormalApproximation {
124        NormalApproximation { capacity: 1.0 - delta, dispersion: delta * (1.0 - delta), half_log: false }
125    }
126
127    /// The real AWGN channel of SNR `snr_lin` with Gaussian codebooks.
128    pub fn awgn(snr_lin: f64) -> NormalApproximation {
129        let p = snr_lin;
130        let v = p * (p + 2.0) / (2.0 * (p + 1.0).powi(2)) * std::f64::consts::LOG2_E.powi(2);
131        NormalApproximation { capacity: 0.5 * (1.0 + p).log2(), dispersion: v, half_log: true }
132    }
133
134    /// The binary-input AWGN channel of noise variance `noise_var` per dimension.
135    pub fn biawgn(noise_var: f64) -> NormalApproximation {
136        let (m1, m2) = moments(noise_var);
137        NormalApproximation { capacity: m1, dispersion: (m2 - m1 * m1).max(0.0), half_log: true }
138    }
139
140    /// $\log_{2} M^{*}(n, \varepsilon)$: the largest number of information bits of a code of
141    /// length `n` at the block error probability `eps`.
142    pub fn max_bits(&self, n: usize, eps: f64) -> f64 {
143        let nf = n as f64;
144        nf * self.capacity - (nf * self.dispersion).sqrt() * comnum_math::qfuncinv(eps) + self.third(nf)
145    }
146
147    /// The smallest block error probability of a code of length `n` carrying `k` bits.
148    pub fn block_error(&self, n: usize, k: usize) -> f64 {
149        let nf = n as f64;
150        qfunc((nf * self.capacity - k as f64 + self.third(nf)) / (nf * self.dispersion).sqrt())
151    }
152
153    /// The smallest blocklength that reaches the rate `rate` (bits per channel use) at the
154    /// block error probability `eps` (real-valued, by bisection), or `None` beyond
155    /// capacity.
156    pub fn blocklength(&self, rate: f64, eps: f64) -> Option<f64> {
157        if rate >= self.capacity {
158            return None;
159        }
160        let gap = |n: f64| {
161            n * (self.capacity - rate) - (n * self.dispersion).sqrt() * comnum_math::qfuncinv(eps) + self.third(n)
162        };
163        let mut hi = 2.0;
164        while gap(hi) < 0.0 {
165            hi *= 2.0;
166            if hi > 1e12 {
167                return None;
168            }
169        }
170        Some(comnum_math::bisect(|n| gap(n) < 0.0, 1.0, hi, 200))
171    }
172
173    fn third(&self, n: f64) -> f64 {
174        if self.half_log { 0.5 * n.log2() } else { 0.0 }
175    }
176}
177
178/// The smallest $E_{b}/N_{0}$ (dB) at which BPSK supports the rate `rate`
179/// ($n \to \infty$): the solution of $C = R$.
180pub fn shannon_limit_ebn0_db(rate: f64) -> f64 {
181    comnum_math::bisect(|e| biawgn_capacity(bpsk_noise_variance(e, rate)) < rate, -2.0, 20.0, 100)
182}
183
184/// The smallest $E_{b}/N_{0}$ (dB) of the AWGN channel at the spectral efficiency `eta`
185/// (bits per two dimensions) with Gaussian inputs: $E_{b}/N_{0} > (2^{\eta} - 1)/\eta$,
186/// which tends to $\ln 2$ ($-1.59$ dB, the ultimate Shannon limit) as $\eta \to 0$. A
187/// binary code of rate $R$ on BPSK has $\eta = 2R$; [`shannon_limit_ebn0_db`] is its
188/// binary-input limit, above this one.
189///
190/// # Examples
191/// ```
192/// use comnum_fec::bounds::{gaussian_limit_ebn0_db, shannon_limit_ebn0_db};
193/// assert!((gaussian_limit_ebn0_db(0.0) + 1.592).abs() < 1e-3);
194/// // binary inputs cost 0.19 dB at rate 1/2
195/// assert!((shannon_limit_ebn0_db(0.5) - gaussian_limit_ebn0_db(1.0) - 0.187).abs() < 0.01);
196/// ```
197pub fn gaussian_limit_ebn0_db(eta: f64) -> f64 {
198    let lin = if eta.abs() < 1e-9 { std::f64::consts::LN_2 } else { (eta.exp2() - 1.0) / eta };
199    comnum_math::lin2db(lin)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn the_gaussian_limit_is_continuous_at_zero() {
208        assert!((gaussian_limit_ebn0_db(1e-6) - gaussian_limit_ebn0_db(0.0)).abs() < 1e-5);
209        assert!(gaussian_limit_ebn0_db(1.0).abs() < 1e-12);
210        // the binary-input limit lies above the Gaussian one
211        for r in [0.1, 0.25, 0.5, 0.75, 0.9] {
212            assert!(shannon_limit_ebn0_db(r) > gaussian_limit_ebn0_db(2.0 * r));
213        }
214    }
215
216    /// The quadrature as written before `gaussian_nodes`: the nodes scaled by hand.
217    fn former_moments(noise_var: f64) -> (f64, f64) {
218        let (t, w) = comnum_math::gauss_hermite(64);
219        let (mu, s) = (2.0 / noise_var, 2.0 / noise_var.sqrt());
220        let sp = std::f64::consts::PI.sqrt();
221        let (mut m1, mut m2) = (0.0, 0.0);
222        for (ti, wi) in t.iter().zip(&w) {
223            let i = info_density(mu + std::f64::consts::SQRT_2 * s * ti);
224            m1 += wi * i / sp;
225            m2 += wi * i * i / sp;
226        }
227        (m1, m2)
228    }
229
230    #[test]
231    fn the_shared_nodes_give_the_former_moments_to_the_rounding() {
232        for ebn0_db in [-2.0, 0.0, 2.0, 4.0, 8.0] {
233            let s2 = bpsk_noise_variance(ebn0_db, 0.5);
234            let ((a1, a2), (b1, b2)) = (moments(s2), former_moments(s2));
235            assert!((a1 - b1).abs() <= 1e-14 && (a2 - b2).abs() <= 1e-14, "{ebn0_db} dB: {a1} {b1}, {a2} {b2}");
236        }
237    }
238
239    #[test]
240    fn capacity_limits_and_monotonicity() {
241        assert!(biawgn_capacity(1e-3) > 0.999_999);
242        assert!(biawgn_capacity(1e3) < 1e-3);
243        // C(σ² = 1) = 0.4859 (the BI-AWGN capacity at Es/N0 = -3 dB)
244        assert!((biawgn_capacity(1.0) - 0.4859).abs() < 2e-3);
245        // the rate-1/3 limit is -0.495 dB, rate 1/2 0.187 dB
246        assert!((shannon_limit_ebn0_db(1.0 / 3.0) + 0.495).abs() < 0.02);
247        // longer codes get closer to capacity
248        assert!(normal_approximation(1024, 512, 1.5) < normal_approximation(128, 64, 1.5));
249    }
250
251    #[test]
252    fn the_approximation_inverts_itself() {
253        let c = NormalApproximation::bsc(0.11);
254        let k = c.max_bits(1000, 1e-3);
255        assert!((c.block_error(1000, k.round() as usize) / 1e-3).ln().abs() < 0.2);
256        assert!(NormalApproximation::bec(0.5).blocklength(0.5, 1e-3).is_none());
257    }
258}
259
260/// Checks against the published tables (`references/`).
261#[cfg(test)]
262mod literature {
263    use super::*;
264
265    /// Y. Polyanskiy, H. V. Poor and S. Verdú, "Channel coding rate in the finite
266    /// blocklength regime," IEEE Trans. Inf. Theory 56(5), 2307–2359, 2010, Table I, the
267    /// column "Normal Approx.": the blocklength that reaches $R = 0.9 C$ is about 955 for
268    /// the BEC(0.5) at $\varepsilon = 10^{-3}$, 3150 for the BSC(0.11), 2750 for the AWGN
269    /// channel at 0 dB and $10^{-3}$, 190 at 20 dB and $10^{-6}$ (here 955.0, 3164, 2748,
270    /// 188.8: within 1 %).
271    #[test]
272    fn polyanskiy_poor_verdu_2010_table_i() {
273        let cases = [
274            (NormalApproximation::bec(0.5), 1e-3, 955.0),
275            (NormalApproximation::bsc(0.11), 1e-3, 3150.0),
276            (NormalApproximation::awgn(1.0), 1e-3, 2750.0),
277            (NormalApproximation::awgn(100.0), 1e-6, 190.0),
278        ];
279        for (c, eps, want) in cases {
280            let n = c.blocklength(0.9 * c.capacity, eps).unwrap();
281            assert!((n / want - 1.0).abs() < 0.01, "{c:?}: n = {n} vs {want}");
282        }
283    }
284}