Skip to main content

comnum_mapping/
bicm.rs

1//! Cutoff rates of coded modulation (CM) and bit-interleaved coded modulation (BICM)
2//! with uniform inputs (Caire, Taricco & Biglieri 1998, Sec. III); their capacities are
3//! the mutual information and the generalized mutual information of
4//! [`crate::theory::constellation_mi`] and [`crate::theory::constellation_gmi`].
5//!
6//! A constellation $\mathcal{X}$ of $M = 2^{m}$ points with unit mean energy carries
7//! the label of `points()[i]` in the bits of `i` (Gray for the families of
8//! [`Constellation::new`], set partitioning for [`Constellation::set_partitioning`]).
9//! The channel is $y = g x + n$, $n \sim \mathcal{CN}(0, \sigma^{2})$ (a real
10//! constellation sees a real channel, as in [`crate::theory`]), the SNR is
11//! $1/\sigma^{2}$, and the gain $g$ is 1 ([`RateChannel::Awgn`]) or Rayleigh,
12//! $\mathbb{E}|g|^{2} = 1$, known at the receiver ([`RateChannel::Rayleigh`], perfect CSI
13//! and ideal interleaving; [`RateChannel`] is that of [`crate::theory`]). With $\mathcal{X}_{b}^{i}$ the points whose bit $i$ is $b$:
14//!
15//! - the CM capacity (eq. (10)), the mutual information
16//!   ([`crate::theory::constellation_mi`] with `px = None`),
17//!   $\tilde{C} = m - \mathbb{E}\left[\log_{2} \sum_{z \in \mathcal{X}} p(y|z) / p(y|x)\right]$;
18//! - the BICM capacity (eq. (14)) ([`crate::theory::constellation_gmi`] with
19//!   `px = None`),
20//!   $\hat{C} = m - \sum_{i=1}^{m} \mathbb{E}_{b,y}\left[\log_{2} \sum_{z \in \mathcal{X}} p(y|z)
21//!   / \sum_{z \in \mathcal{X}_{b}^{i}} p(y|z)\right] \le \tilde{C}$ (eq. (16)), the
22//!   generalized mutual information;
23//! - the CM cutoff rate (eq. (17)), in closed form, [`cm_cutoff_rate`],
24//!   $\tilde{R}_{0} = m - \log_{2} \frac{1}{M} \sum_{x, z} \mathbb{E}_{g}\, e^{-|g|^{2}
25//!   |x - z|^{2} / (4\sigma^{2})}$, where the mean over Rayleigh fading is
26//!   $1 / (1 + |x - z|^{2} / (4\sigma^{2}))$;
27//! - the BICM cutoff rate (eqs. (19), (21)), [`bicm_cutoff_rate`],
28//!   $\hat{R}_{0} = m\left(1 - \log_{2}(1 + B)\right)$ with the Bhattacharyya factor
29//!   $B = \frac{1}{m} \sum_{i} \mathbb{E}_{b,y}\sqrt{\sum_{z \in \mathcal{X}_{\bar{b}}^{i}}
30//!   p(y|z) / \sum_{z \in \mathcal{X}_{b}^{i}} p(y|z)}$.
31//!
32//! The expectations over the noise use Gauss–Hermite quadrature with `points` nodes
33//! per real dimension (the paper uses Monte Carlo); the mean over Rayleigh fading is
34//! the integral over $u = \ln |g|^{2}$, of density $e^{u - e^{u}}$, by Simpson's rule
35//! with a step of 1/8.
36//!
37//! # Examples
38//! ```
39//! use comnum_mapping::bicm::bicm_cutoff_rate;
40//! use comnum_mapping::constellation::{Constellation, ConstellationKind};
41//! use comnum_mapping::theory::{RateChannel, constellation_gmi, constellation_mi};
42//! // 16-QAM at 10 dB: BICM with Gray labels loses little, with set partitioning much
43//! let gray = Constellation::qam(16)?;
44//! let sp = Constellation::set_partitioning(ConstellationKind::Qam, 16)?;
45//! let cm = constellation_mi(&gray, 10.0, None, RateChannel::Awgn, 16)?;
46//! let g = constellation_gmi(&gray, 10.0, None, RateChannel::Awgn, 16)?;
47//! let s = constellation_gmi(&sp, 10.0, None, RateChannel::Awgn, 16)?;
48//! assert!(cm > g && cm - g < 0.05 && g - s > 0.3);
49//! // the cutoff rate lies below the capacity
50//! assert!(bicm_cutoff_rate(&gray, 10.0, RateChannel::Awgn, 16)? < g);
51//! # Ok::<(), comnum_core::Error>(())
52//! ```
53//!
54//! # References
55//!
56//! 1. G. Caire, G. Taricco and E. Biglieri, "Bit-interleaved coded modulation," *IEEE
57//!    Trans. Inf. Theory*, 44(3), 927–946, 1998.
58//! 2. E. Zehavi, "8-PSK trellis codes for a Rayleigh channel," *IEEE Trans. Commun.*,
59//!    40(5), 873–884, 1992.
60
61use crate::constellation::Constellation;
62use crate::theory::{RateChannel, TheorySetup, bit_expectations, over_fading, setup};
63use comnum_core::Result;
64use comnum_math::{db2lin, log_sum_exp_iter};
65
66/// The CM cutoff rate (bits per channel use) of a constellation with uniform inputs,
67/// eq. (17) of Caire et al. 1998 in closed form (see the module).
68///
69/// # Examples
70/// ```
71/// use comnum_mapping::bicm::cm_cutoff_rate;
72/// use comnum_mapping::theory::RateChannel;
73/// use comnum_mapping::constellation::Constellation;
74/// // BPSK: 1 - log2(1 + e^{-Es/N0})
75/// let c = Constellation::psk(2)?;
76/// let r0 = cm_cutoff_rate(&c, 0.0, RateChannel::Awgn);
77/// assert!((r0 - (1.0 - (1.0 + (-1f64).exp()).log2())).abs() < 1e-12);
78/// # Ok::<(), comnum_core::Error>(())
79/// ```
80pub fn cm_cutoff_rate(c: &Constellation, snr_db: f64, fading: RateChannel) -> f64 {
81    let x = c.normalized();
82    let x = x.points();
83    let m = x.len() as f64;
84    // |x - z|²/(4σ²) for a complex channel, |x - z|²/(8σ²) for a real one
85    let scale = db2lin(snr_db) / if c.kind().is_real() { 8.0 } else { 4.0 };
86    let mut sum = 0.0;
87    for xi in x {
88        for xj in x {
89            let a = scale * (xi - xj).norm_sqr();
90            sum += match fading {
91                RateChannel::Awgn => (-a).exp(),
92                RateChannel::Rayleigh => 1.0 / (1.0 + a),
93            };
94        }
95    }
96    m.log2() - (sum / m).log2()
97}
98
99/// The BICM cutoff rate (bits per channel use) of a labelled constellation with uniform
100/// inputs, eqs. (19) and (21) of Caire et al. 1998 (see the module).
101///
102/// # Errors
103/// [`comnum_core::Error::Config`] for an invalid constellation.
104pub fn bicm_cutoff_rate(c: &Constellation, snr_db: f64, fading: RateChannel, points: usize) -> Result<f64> {
105    let b = c.bits_per_symbol() as f64;
106    let factor = over_fading(|s| Ok(bhattacharyya(&setup(c, None, s, points)?)), snr_db, fading)?;
107    Ok(b * (1.0 - factor.ln_1p() / std::f64::consts::LN_2))
108}
109
110/// The Bhattacharyya factor $B$ of eq. (19), from the log-metrics of every bit.
111fn bhattacharyya(s: &TheorySetup) -> f64 {
112    // √(Σ_{other} / Σ_{same}) = e^{(ln Σ_{other} - ln Σ_{same})/2}, with ln Σ_{other}
113    // = ln(Σ_all - Σ_same) computed without cancellation from the log-sums
114    let terms = bit_expectations(s, |all, same| {
115        let total = log_sum_exp_iter(all.iter().copied());
116        let other = total + (-(same - total).exp()).ln_1p();
117        (0.5 * (other - same)).exp()
118    });
119    terms.iter().sum::<f64>() / terms.len() as f64
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::constellation::ConstellationKind;
126    use crate::theory::{constellation_gmi, constellation_mi};
127    use comnum_core::rng::Rng;
128
129    #[test]
130    fn the_rayleigh_mean_of_the_cm_cutoff_rate_is_its_closed_form() {
131        // the numerical mean over fading of the AWGN closed form (inside the log)
132        let c = Constellation::qam(16).unwrap();
133        let m = 16f64;
134        for snr_db in [-5.0, 5.0, 20.0, 40.0] {
135            let inner = |s: f64| Ok((m.log2() - cm_cutoff_rate(&c, s, RateChannel::Awgn)).exp2());
136            let mean = over_fading(inner, snr_db, RateChannel::Rayleigh).unwrap();
137            let want = m.log2() - mean.log2();
138            let got = cm_cutoff_rate(&c, snr_db, RateChannel::Rayleigh);
139            assert!((got - want).abs() < 1e-6, "{snr_db} dB: {got} vs {want}");
140        }
141    }
142
143    #[test]
144    fn the_capacities_over_rayleigh_fading_match_a_simulation() {
145        // Monte Carlo of the information densities of eqs. (10) and (14), 16-QAM Gray
146        let c = Constellation::qam(16).unwrap().normalized();
147        let x = c.points();
148        let snr_db = 8.0;
149        let sigma2 = db2lin(-snr_db);
150        let mut r = Rng::new(11);
151        let n = 40_000;
152        let (mut cm, mut bicm) = (Vec::with_capacity(n), Vec::with_capacity(n));
153        for _ in 0..n {
154            let i = r.below(16);
155            let g = r.complex_gaussian();
156            let y = g * x[i] + r.complex_gaussian() * sigma2.sqrt();
157            let l: Vec<f64> = x.iter().map(|z| -(y - g * z).norm_sqr() / sigma2).collect();
158            let all = log_sum_exp_iter(l.iter().copied());
159            cm.push(4.0 - (all - l[i]) / std::f64::consts::LN_2);
160            let mut v = 4.0;
161            for k in 0..4 {
162                let bit = (i >> (3 - k)) & 1;
163                let same = log_sum_exp_iter((0..16).filter(|&j| (j >> (3 - k)) & 1 == bit).map(|j| l[j]));
164                v -= (all - same) / std::f64::consts::LN_2;
165            }
166            bicm.push(v);
167        }
168        for (v, want) in [
169            (&cm, constellation_mi(&c, snr_db, None, RateChannel::Rayleigh, 16).unwrap()),
170            (&bicm, constellation_gmi(&c, snr_db, None, RateChannel::Rayleigh, 16).unwrap()),
171        ] {
172            let mean = v.iter().sum::<f64>() / n as f64;
173            let sd = (v.iter().map(|a| (a - mean).powi(2)).sum::<f64>() / (n * (n - 1)) as f64).sqrt();
174            assert!((mean - want).abs() < 4.0 * sd, "{mean} ± {sd} vs {want}");
175        }
176    }
177
178    #[test]
179    fn the_bhattacharyya_factor_of_bpsk_is_closed() {
180        // one bit: B = e^{-Es/N0} over AWGN, 1/(1 + Es/N0) over Rayleigh fading
181        let c = Constellation::psk(2).unwrap();
182        for snr_db in [-3.0, 0.0, 6.0] {
183            let s = db2lin(snr_db);
184            for (fading, b) in [(RateChannel::Awgn, (-s).exp()), (RateChannel::Rayleigh, 1.0 / (1.0 + s))] {
185                let want = 1.0 - b.ln_1p() / std::f64::consts::LN_2;
186                let got = bicm_cutoff_rate(&c, snr_db, fading, 40).unwrap();
187                assert!((got - want).abs() < 1e-6, "{snr_db} dB {fading:?}: {got} vs {want}");
188                assert!((got - cm_cutoff_rate(&c, snr_db, fading)).abs() < 1e-6);
189            }
190        }
191    }
192
193    #[test]
194    fn bicm_never_beats_cm_capacity_and_gray_beats_set_partitioning() {
195        for (kind, m) in [(ConstellationKind::Psk, 8), (ConstellationKind::Qam, 16)] {
196            let gray = Constellation::new(kind, m).unwrap();
197            let sp = Constellation::set_partitioning(kind, m).unwrap();
198            for snr_db in [0.0, 6.0, 12.0] {
199                let cm = constellation_mi(&gray, snr_db, None, RateChannel::Awgn, 16).unwrap();
200                let g = constellation_gmi(&gray, snr_db, None, RateChannel::Awgn, 16).unwrap();
201                let s = constellation_gmi(&sp, snr_db, None, RateChannel::Awgn, 16).unwrap();
202                assert!(cm >= g - 1e-12 && g > s, "{kind:?} {snr_db} dB: {cm} {g} {s}");
203                // relabelling does not change the CM capacity
204                assert!((constellation_mi(&sp, snr_db, None, RateChannel::Awgn, 16).unwrap() - cm).abs() < 1e-9);
205            }
206        }
207    }
208}
209
210#[cfg(test)]
211mod literature {
212    use super::*;
213    use crate::constellation::ConstellationKind;
214    use crate::theory::{constellation_gmi, constellation_mi};
215    use comnum_math::bisect;
216
217    fn pair(kind: ConstellationKind, m: usize) -> (Constellation, Constellation) {
218        (Constellation::new(kind, m).unwrap(), Constellation::set_partitioning(kind, m).unwrap())
219    }
220
221    /// G. Caire, G. Taricco and E. Biglieri, "Bit-interleaved coded modulation," IEEE
222    /// Trans. Inf. Theory 44(3), 1998, Figs. 4 (AWGN) and 5 (Rayleigh fading, perfect
223    /// CSI): values read on the figures within 0.1 bit (AWGN at 10 dB: CM 8PSK 2.7, CM
224    /// 16QAM 3.15, BICM-SP 16QAM 2.4; Rayleigh: CM 16QAM 2.6 at 10 dB, BICM-SP 16QAM 3.0
225    /// at 15 dB), and the text: Gray-labelled BICM is "very close" to CM (within 0.1
226    /// bit from -5 to 20 dB), set partitioning "several decibels worse" (2.3 dB more
227    /// SNR for 2 bits with 8PSK, 3.8 dB with 16QAM).
228    #[test]
229    fn caire_1998_figs_4_5_capacity() {
230        let (p8, _) = pair(ConstellationKind::Psk, 8);
231        let (q16, sp16) = pair(ConstellationKind::Qam, 16);
232        for (got, want) in [
233            (constellation_mi(&p8, 10.0, None, RateChannel::Awgn, 12).unwrap(), 2.7),
234            (constellation_mi(&q16, 10.0, None, RateChannel::Awgn, 12).unwrap(), 3.15),
235            (constellation_gmi(&sp16, 10.0, None, RateChannel::Awgn, 12).unwrap(), 2.4),
236            (constellation_mi(&q16, 10.0, None, RateChannel::Rayleigh, 8).unwrap(), 2.6),
237            (constellation_gmi(&sp16, 15.0, None, RateChannel::Rayleigh, 8).unwrap(), 3.0),
238        ] {
239            assert!((got - want).abs() < 0.1, "{got} vs {want}");
240        }
241        for (kind, m) in [(ConstellationKind::Psk, 4), (ConstellationKind::Psk, 8), (ConstellationKind::Qam, 16)] {
242            let (gray, sp) = pair(kind, m);
243            for snr_db in [-5.0, 0.0, 5.0, 10.0, 20.0] {
244                let gap = constellation_mi(&gray, snr_db, None, RateChannel::Awgn, 12).unwrap()
245                    - constellation_gmi(&gray, snr_db, None, RateChannel::Awgn, 12).unwrap();
246                assert!((-1e-12..0.1).contains(&gap), "{kind:?} {m}, {snr_db} dB: {gap}");
247            }
248            if m > 4 {
249                let need = |c: &Constellation| {
250                    bisect(|s| constellation_gmi(c, s, None, RateChannel::Awgn, 12).unwrap() < 2.0, -10.0, 30.0, 30)
251                };
252                let loss = need(&sp) - need(&gray);
253                assert!((2.0..6.0).contains(&loss), "{kind:?} {m}: {loss} dB");
254            }
255        }
256    }
257}