comnum_mapping/theory.rs
1//! Closed-form error rates and information rates of constellations over AWGN.
2//!
3//! - [`ber`] (Gray-mapped QAM, PSK and PAM, OptiCommPy `theoryBER`) and [`ser`] give the
4//! error rates as functions of $E_{b}/N_{0}$, and [`ebn0_for_ber`], [`ebn0_for_ser`]
5//! their inverses;
6//! - [`ber_rayleigh_mrc`] gives the BER of BPSK and Gray QPSK with maximal-ratio
7//! combining of independent Rayleigh branches;
8//! - [`mi`] and [`gmi`] give the symbol-wise and bit-wise mutual information by
9//! Gauss–Hermite quadrature (OptiCommPy `theoryMI`, `theoryGMI`), and
10//! [`constellation_mi`], [`constellation_gmi`] the same for any constellation and
11//! labelling, over AWGN or Rayleigh fading ([`RateChannel`]): the capacities of coded
12//! modulation and BICM;
13//! - [`ber_dpsk`], [`ber_dbpsk_coherent`] and [`differential_phase_tail`] give the error
14//! rates of differential PSK ([`crate::differential`]);
15//! - [`ber_gray_pam`], [`ber_gray_qam`] and [`ber_gray_rect_qam`] give the exact BER of
16//! Gray-mapped PAM, square and rectangular QAM (Cho and Yoon 2002), [`ber_exact`] the
17//! exact BER of a family where it exists, and [`phase_pdf`]
18//! the density of the phase of a carrier in Gaussian noise;
19//! - [`ser_orthogonal_noncoherent`] and [`ber_orthogonal_noncoherent`] give the SER and
20//! the BER of $M$ orthogonal signals detected noncoherently (M-FSK, chirp spread
21//! spectrum).
22//!
23//! The closed forms are functions of $E_{b}/N_{0}$ (`ebn0_db`) unless their name or
24//! their argument says otherwise: [`ser_esn0`] and the orthogonal signals take
25//! $E_{s}/N_{0}$ (`esn0_db`).
26//!
27//! They are properties of a constellation and its labelling, so they live with the
28//! constellations; `comnum-metrics` re-exports them as `theory_ber`, `theory_ser`,
29//! `theory_mi` and `theory_gmi`. The functions of a family of constellations take the
30//! family and the order first: `(kind, m, …)`.
31//!
32//! # Model
33//!
34//! For BPSK over AWGN,
35//!
36//! $$
37//! P_{b} = Q\left(\sqrt{2 E_{b} / N_{0}}\right)
38//! $$
39//!
40//! # Examples
41//! ```
42//! use comnum_mapping::{theory, constellation::ConstellationKind};
43//! let pb = theory::ber(ConstellationKind::Qam, 4, 6.0).unwrap();
44//! assert!((pb - 2.388e-3).abs() < 1e-6);
45//! ```
46//!
47//! # References
48//!
49//! 1. J. G. Proakis and M. Salehi, *Digital Communications*, 5th ed., McGraw-Hill, 2008.
50//! 2. R. F. Pawula, S. O. Rice and J. H. Roberts, "Distribution of the phase angle
51//! between two vectors perturbed by Gaussian noise," *IEEE Trans. Commun.*, 30(8),
52//! 1828–1841, 1982.
53
54use crate::constellation::{Constellation, ConstellationKind, log_priors, qam_side};
55use crate::demapper::LabelHalves;
56use comnum_core::{Error, Result, ensure};
57use comnum_math::{C64, Jacobian, bessel_i0e, bisect, db2lin, entropy, log_sum_exp, qfunc};
58
59/// The orders of the closed forms of [`ber`] and [`ser`]: a power of two of at least 2,
60/// a square for QAM.
61fn ensure_order(kind: ConstellationKind, m: usize) -> Result<()> {
62 ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
63 ensure(kind != ConstellationKind::Qam || qam_side(m).is_some(), || {
64 format!("the closed forms of QAM need a square order, got {m}")
65 })
66}
67
68/// Theoretical bit error rate over AWGN with Gray mapping (OptiCommPy `theoryBER`).
69///
70/// - QAM (`L = √M`): `P_b = 2(1 - 1/L)/log2 L · Q(√(3 log2 L/(L²-1) · 2 E_b/N_0))`;
71/// - PSK: `P_b = 2 Q(√(2 log2 M · E_b/N_0) sin(π/M)) / log2 M`, and the exact
72/// `Q(√(2 E_b/N_0))` for `M = 2` (OptiCommPy applies the general formula, which counts
73/// two nearest neighbours and doubles the BER of BPSK);
74/// - PAM: `P_b = 2(M-1)/M · Q(√(6 log2 M/(M²-1) · E_b/N_0)) / log2 M`.
75///
76/// # Errors
77/// [`Error::Config`] for another family, an order that is not a power of two (at least
78/// 2), or a QAM that is not square.
79pub fn ber(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
80 ensure_order(kind, m)?;
81 let g = db2lin(ebn0_db);
82 let mf = m as f64;
83 let k = mf.log2();
84 match kind {
85 ConstellationKind::Qam => {
86 let l = mf.sqrt();
87 Ok(2.0 * (1.0 - 1.0 / l) / l.log2() * qfunc((3.0 * l.log2() / (l * l - 1.0) * 2.0 * g).sqrt()))
88 }
89 ConstellationKind::Psk if m == 2 => Ok(qfunc((2.0 * g).sqrt())),
90 ConstellationKind::Psk => Ok(2.0 * qfunc((2.0 * k * g).sqrt() * (std::f64::consts::PI / mf).sin()) / k),
91 ConstellationKind::Pam => Ok(2.0 * (mf - 1.0) / mf * qfunc((6.0 * k / (mf * mf - 1.0) * g).sqrt()) / k),
92 other => Err(Error::Config(format!("no theoretical BER for {other:?}"))),
93 }
94}
95
96/// Theoretical symbol error rate over AWGN as a function of `E_b/N_0`.
97///
98/// - square QAM: exact, `P_s = 1 - (1 - p)²` with `p = 2(1 - 1/L) Q(√(3 E_s/N_0/(M-1)))`;
99/// - PAM: exact, `P_s = 2(M-1)/M · Q(√(6 E_s/N_0/(M²-1)))`;
100/// - PSK: the nearest-neighbour bound `2 Q(√(2 E_s/N_0) sin(π/M))` (exact for `M = 2`
101/// when halved, tight for `M ≥ 4`).
102///
103/// # Errors
104/// As [`ber`].
105pub fn ser(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
106 ensure_order(kind, m)?;
107 ser_at(kind, m, db2lin(ebn0_db) * (m as f64).log2())
108}
109
110/// [`ser`] as a function of $E_{s}/N_{0}$ (dB), the SNR per symbol: the entry point of
111/// the closed forms whose SNR is per symbol (CE-OFDM, a symbol per subcarrier).
112///
113/// # Errors
114/// As [`ber`].
115///
116/// # Examples
117/// ```
118/// use comnum_mapping::{constellation::ConstellationKind, theory};
119/// // QPSK carries 2 bits: Es/N0 = Eb/N0 + 3.01 dB
120/// let (a, b) = (theory::ser_esn0(ConstellationKind::Qam, 4, 10.0 + 10.0 * 2f64.log10())?, theory::ser(ConstellationKind::Qam, 4, 10.0)?);
121/// assert!((a / b - 1.0).abs() < 1e-12);
122/// # Ok::<(), comnum_core::Error>(())
123/// ```
124pub fn ser_esn0(kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
125 ensure_order(kind, m)?;
126 ser_at(kind, m, db2lin(esn0_db))
127}
128
129/// The SER of [`ser`] at the linear $E_{s}/N_{0}$ `es`.
130fn ser_at(kind: ConstellationKind, m: usize, es: f64) -> Result<f64> {
131 let mf = m as f64;
132 match kind {
133 ConstellationKind::Qam => {
134 let l = mf.sqrt();
135 let p = 2.0 * (1.0 - 1.0 / l) * qfunc((3.0 * es / (mf - 1.0)).sqrt());
136 Ok(1.0 - (1.0 - p) * (1.0 - p))
137 }
138 ConstellationKind::Pam => Ok(2.0 * (mf - 1.0) / mf * qfunc((6.0 * es / (mf * mf - 1.0)).sqrt())),
139 ConstellationKind::Psk => {
140 let p = 2.0 * qfunc((2.0 * es).sqrt() * (std::f64::consts::PI / mf).sin());
141 Ok(if m == 2 { p / 2.0 } else { p })
142 }
143 other => Err(Error::Config(format!("no theoretical SER for {other:?}"))),
144 }
145}
146
147/// The average SER of `m`-PSK or square `m`-QAM over a fading channel whose SNR per
148/// symbol $\gamma$ has the moment generating function `mgf` $(s) = \mathbb{E}\,
149/// e^{s\gamma}$, $s \le 0$, by the MGF approach with Craig's form of the $Q$ function
150/// (E. Basar et al., "Wireless communications through reconfigurable intelligent
151/// surfaces," *IEEE Access*, 2019, eqs. (16) and (21)): with
152/// $g = \sin^{2}(\pi/M)$,
153///
154/// $$
155/// P_{e}^{\mathrm{PSK}} = \frac{1}{\pi} \int_{0}^{(M-1)\pi/M} M_{\gamma}\Bigl(-\frac{g}{\sin^{2}\eta}\Bigr) d\eta ,
156/// $$
157///
158/// and with $c = 3/(2(M-1))$ and $q = 1 - 1/\sqrt{M}$,
159///
160/// $$
161/// P_{e}^{\mathrm{QAM}} = \frac{4q}{\pi} \int_{0}^{\pi/2} M_{\gamma}\Bigl(-\frac{c}{\sin^{2}\eta}\Bigr) d\eta -
162/// \frac{4q^{2}}{\pi} \int_{0}^{\pi/4} M_{\gamma}\Bigl(-\frac{c}{\sin^{2}\eta}\Bigr) d\eta ,
163/// $$
164///
165/// by Gauss–Legendre quadrature of 96 nodes on every interval. With the MGF
166/// $e^{s\gamma_{0}}$ of a constant SNR, these are the SERs of [`ser_esn0`] over AWGN.
167///
168/// # Errors
169/// [`Error::Config`] for another family or an order that is not a power of two (and,
170/// for QAM, not a square).
171///
172/// # Examples
173/// ```
174/// use comnum_mapping::{theory, constellation::ConstellationKind};
175/// // BPSK over Rayleigh fading of mean SNR 10: M(s) = 1/(1 - 10 s)
176/// let p = theory::ser_mgf(ConstellationKind::Psk, 2, |s| 1.0 / (1.0 - 10.0 * s))?;
177/// assert!((p - 0.5 * (1.0 - (10.0f64 / 11.0).sqrt())).abs() < 1e-12);
178/// # Ok::<(), comnum_core::Error>(())
179/// ```
180pub fn ser_mgf(kind: ConstellationKind, m: usize, mgf: impl Fn(f64) -> f64) -> Result<f64> {
181 ensure_order(kind, m)?;
182 let mf = m as f64;
183 let (x, w) = comnum_math::gauss_legendre(96);
184 // ∫_0^b M(-c / sin² η) dη
185 let integral = |c: f64, b: f64| -> f64 {
186 x.iter().zip(&w).map(|(t, wt)| wt * mgf(-c / (b / 2.0 * (t + 1.0)).sin().powi(2))).sum::<f64>() * b / 2.0
187 };
188 match kind {
189 ConstellationKind::Psk => {
190 let g = (std::f64::consts::PI / mf).sin().powi(2);
191 Ok(integral(g, (mf - 1.0) * std::f64::consts::PI / mf) / std::f64::consts::PI)
192 }
193 ConstellationKind::Qam => {
194 let (c, q) = (1.5 / (mf - 1.0), 1.0 - 1.0 / mf.sqrt());
195 let pi = std::f64::consts::PI;
196 Ok(4.0 * q / pi * integral(c, pi / 2.0) - 4.0 * q * q / pi * integral(c, pi / 4.0))
197 }
198 other => Err(Error::Config(format!("no SER by the MGF for {other:?}"))),
199 }
200}
201
202/// The $E_{b}/N_{0}$ (dB) at which the BER of [`ber`] equals `target`: the bisection
203/// (200 steps) of $[-10, 60]$ dB, where the BER decreases.
204///
205/// # Errors
206/// [`Error::Config`] for a family without a closed form, or a target that no
207/// $E_{b}/N_{0}$ of $[-10, 60]$ dB reaches.
208///
209/// # Examples
210/// ```
211/// use comnum_mapping::{theory, constellation::ConstellationKind};
212/// // QPSK: Q(√(2 Eb/N0)) = 1e-3 at 6.79 dB
213/// let e = theory::ebn0_for_ber(ConstellationKind::Qam, 4, 1e-3).unwrap();
214/// assert!((e - 6.79).abs() < 0.01);
215/// ```
216pub fn ebn0_for_ber(kind: ConstellationKind, m: usize, target: f64) -> Result<f64> {
217 ebn0_for(|e| ber(kind, m, e), "BER", target)
218}
219
220/// The $E_{b}/N_{0}$ (dB) at which the SER of [`ser`] equals `target`, as
221/// [`ebn0_for_ber`].
222///
223/// # Errors
224/// As [`ebn0_for_ber`].
225///
226/// # Examples
227/// ```
228/// use comnum_mapping::{theory, constellation::ConstellationKind};
229/// let e = theory::ebn0_for_ser(ConstellationKind::Qam, 16, 1e-4).unwrap();
230/// let s = theory::ser(ConstellationKind::Qam, 16, e).unwrap();
231/// assert!((s / 1e-4 - 1.0).abs() < 1e-9);
232/// ```
233pub fn ebn0_for_ser(kind: ConstellationKind, m: usize, target: f64) -> Result<f64> {
234 ebn0_for(|e| ser(kind, m, e), "SER", target)
235}
236
237/// The root of `rate(e) = target` on `[-10, 60]` dB for a decreasing `rate`.
238fn ebn0_for(rate: impl Fn(f64) -> Result<f64>, what: &str, target: f64) -> Result<f64> {
239 let (a, b) = (-10.0, 60.0);
240 ensure(rate(a)? > target && target > rate(b)?, || format!("no Eb/N0 in [-10, 60] dB gives a {what} of {target}"))?;
241 // the family has a closed form (checked above): `rate` cannot fail on the bracket
242 Ok(bisect(|e| rate(e).is_ok_and(|v| v > target), a, b, 200))
243}
244
245/// The bit error rate of BPSK (or Gray QPSK, per bit) with maximal-ratio combining of
246/// `branches` independent Rayleigh-faded branches of mean SNR per bit `mean_ebn0`
247/// ($\bar\gamma$, linear) each (Proakis & Salehi, *Digital Communications*, 5th ed.,
248/// eq. (13.4-15)):
249///
250/// $$
251/// P_{b} = \left(\frac{1 - \mu}{2}\right)^{L} \sum_{k=0}^{L-1} \binom{L - 1 + k}{k}
252/// \left(\frac{1 + \mu}{2}\right)^{k}, \qquad \mu = \sqrt{\frac{\bar\gamma}{1 + \bar\gamma}},
253/// $$
254///
255/// with $\bar\gamma$ the mean $E_{b}/N_{0}$ per branch (linear) and $L$ the number of
256/// branches; $L = 1$ is the flat Rayleigh channel, $P_{b} = \frac{1}{2}(1 - \mu)$.
257///
258/// # Errors
259/// [`Error::Config`] for no branch.
260///
261/// # Examples
262/// ```
263/// use comnum_mapping::theory::ber_rayleigh_mrc;
264/// let g = comnum_math::db2lin(10.0);
265/// assert!((ber_rayleigh_mrc(g, 1)? - 0.5 * (1.0 - (g / (1.0 + g)).sqrt())).abs() < 1e-15);
266/// // diversity: four branches at 10 dB each
267/// assert!(ber_rayleigh_mrc(g, 4)? < 1e-5);
268/// # Ok::<(), comnum_core::Error>(())
269/// ```
270pub fn ber_rayleigh_mrc(mean_ebn0_lin: f64, branches: usize) -> Result<f64> {
271 ensure(branches >= 1, || "maximal-ratio combining needs at least one branch".into())?;
272 let mu = (mean_ebn0_lin / (1.0 + mean_ebn0_lin)).sqrt();
273 let l = branches;
274 let mut binom = 1.0;
275 let mut sum = 0.0;
276 for k in 0..l {
277 if k > 0 {
278 binom *= (l - 1 + k) as f64 / k as f64;
279 }
280 sum += binom * ((1.0 + mu) / 2.0).powi(k as i32);
281 }
282 Ok(((1.0 - mu) / 2.0).powi(l as i32) * sum)
283}
284
285/// The normalised points, their probabilities, `N_0` and the quadrature nodes of a
286/// theory function.
287pub(crate) struct TheorySetup {
288 /// The points, at unit energy under `probs`; point `i` carries the bits of `i`.
289 pub(crate) points: Vec<C64>,
290 /// The probabilities of the points.
291 pub(crate) probs: Vec<f64>,
292 /// The noise of the metrics, $N_{0}$.
293 pub(crate) n0: f64,
294 /// The quadrature nodes `(w, weight)` of the noise.
295 pub(crate) nodes: Vec<(C64, f64)>,
296}
297
298/// The normalised points, the probabilities and the quadrature of a theory function of
299/// a constellation, its labels included (`points()[i]` carries the bits of `i`).
300pub(crate) fn setup(c: &Constellation, px: Option<&[f64]>, snr_db: f64, points: usize) -> Result<TheorySetup> {
301 let p = c.priors(px)?;
302 let pts = c.normalized_under(&p)?.points().to_vec();
303 // OptiCommPy: a real constellation sees a real channel of variance 1/SNR, a complex
304 // one a complex channel of variance 1/SNR (1/(2 SNR) per dimension)
305 let real = c.kind().is_real();
306 let sigma2 = db2lin(-snr_db);
307 let n0 = if real { 2.0 * sigma2 } else { sigma2 };
308 Ok(TheorySetup { points: pts, probs: p, n0, nodes: comnum_math::gaussian_nodes(sigma2, !real, points) })
309}
310
311/// The channel of [`constellation_mi`], [`constellation_gmi`] and of the cutoff rates of
312/// [`crate::bicm`].
313#[derive(Clone, Copy, Debug, PartialEq, Eq)]
314pub enum RateChannel {
315 /// No fading: $g = 1$.
316 Awgn,
317 /// Flat Rayleigh fading, $g \sim \mathcal{CN}(0, 1)$ independent from symbol to
318 /// symbol (ideal interleaving), known at the receiver.
319 Rayleigh,
320}
321
322impl std::str::FromStr for RateChannel {
323 type Err = Error;
324
325 /// `"awgn"` or `"rayleigh"`.
326 fn from_str(name: &str) -> Result<RateChannel> {
327 match name.to_ascii_lowercase().as_str() {
328 "awgn" => Ok(RateChannel::Awgn),
329 "rayleigh" => Ok(RateChannel::Rayleigh),
330 other => Err(Error::Config(format!("unknown fading {other:?}: awgn, rayleigh"))),
331 }
332 }
333}
334
335/// The mean of `f(snr_db)` over the channel: `f(snr_db)` itself without fading, and
336/// $\mathbb{E}\, f(\mathrm{snr} + 10 \log_{10} |g|^{2})$ over Rayleigh fading, the
337/// integral over $u = \ln |g|^{2}$, of density $e^{u - e^{u}}$, by Simpson's rule with a
338/// step of 1/8.
339pub(crate) fn over_fading(f: impl Fn(f64) -> Result<f64>, snr_db: f64, fading: RateChannel) -> Result<f64> {
340 if fading == RateChannel::Awgn {
341 return f(snr_db);
342 }
343 // below e^{lo} the integrand is f(0) to first order, with the mass 1 - e^{-e^{lo}}
344 let lo = (1e-4 / db2lin(snr_db).max(1.0)).ln();
345 let hi = 40f64.ln();
346 let n = (8.0 * (hi - lo)).ceil() as usize;
347 let at = |u: f64| -> Result<f64> { Ok((u - u.exp()).exp() * f(snr_db + 10.0 * u / std::f64::consts::LN_10)?) };
348 let tail = -(-lo.exp()).exp_m1() * f(snr_db + 10.0 * lo / std::f64::consts::LN_10)?;
349 Ok(comnum_math::try_simpson(at, lo, hi, n)? + tail)
350}
351
352/// The Gauss–Hermite nodes per dimension of the information rates when the caller does
353/// not choose ([`constellation_mi`], [`constellation_gmi`], the BICM cutoff rate): the
354/// default of the `theory` checks of the specifications and of the Python closed forms.
355pub const DEFAULT_QUADRATURE_POINTS: usize = 40;
356
357/// The mutual information (bits/symbol) of the constellation `c` with the probabilities
358/// `px` (uniform if `None`) over the channel `fading`: [`mi`] of any constellation,
359/// averaged over the fading. With uniform inputs it is the capacity of coded modulation
360/// (Caire, Taricco & Biglieri 1998, eq. (10)).
361///
362/// # Errors
363/// [`Error::Config`] for invalid probabilities.
364pub fn constellation_mi(
365 c: &Constellation,
366 snr_db: f64,
367 px: Option<&[f64]>,
368 fading: RateChannel,
369 points: usize,
370) -> Result<f64> {
371 over_fading(|s| Ok(mi_of(&setup(c, px, s, points)?)), snr_db, fading)
372}
373
374/// The generalized mutual information (bits/symbol) of the bit-wise decoder of the
375/// labelled constellation `c` (point `i` carries the bits of `i`) with the probabilities
376/// `px` over the channel `fading`: [`gmi`] of any labelling, averaged over the fading.
377/// With uniform inputs it is the capacity of BICM (Caire, Taricco & Biglieri 1998,
378/// eq. (14)).
379///
380/// # Errors
381/// [`Error::Config`] for invalid probabilities.
382pub fn constellation_gmi(
383 c: &Constellation,
384 snr_db: f64,
385 px: Option<&[f64]>,
386 fading: RateChannel,
387 points: usize,
388) -> Result<f64> {
389 over_fading(|s| Ok(gmi_of(&setup(c, px, s, points)?)), snr_db, fading)
390}
391
392/// Mutual information (bits/symbol) of a constellation over AWGN, by Gauss–Hermite
393/// quadrature (OptiCommPy `theoryMI`).
394///
395/// `snr_db` is `E_s/σ²` with `E_s` the mean energy under `px` (uniform if `None`);
396/// a real constellation (PAM, OOK) sees a real channel, a complex one a circular
397/// complex channel, as in OptiCommPy. With `D_ij(w) = (|x_i - x_j + w|² - |w|²)/N_0`,
398///
399/// `MI = -Σ_i p_i E_w log2 Σ_j p_j e^{-D_ij(w)}`,
400///
401/// evaluated with `points` nodes per dimension (40 give 6 digits for 64-QAM).
402///
403/// # Errors
404/// [`Error::Config`] for an invalid constellation or probabilities.
405///
406/// # Examples
407/// ```
408/// use comnum_mapping::{theory, constellation::ConstellationKind};
409/// // the MI of 16-QAM approaches 4 bits at high SNR and stays below the capacity
410/// let mi = theory::mi(ConstellationKind::Qam, 16, 10.0, None, 40).unwrap();
411/// assert!(mi < (1.0 + 10.0f64).log2() && mi > 2.5);
412/// let hi = theory::mi(ConstellationKind::Qam, 16, 30.0, None, 40).unwrap();
413/// assert!((hi - 4.0).abs() < 1e-6);
414/// ```
415pub fn mi(kind: ConstellationKind, m: usize, snr_db: f64, px: Option<&[f64]>, points: usize) -> Result<f64> {
416 constellation_mi(&Constellation::new(kind, m)?, snr_db, px, RateChannel::Awgn, points)
417}
418
419/// The kernel of [`mi`].
420pub(crate) fn mi_of(s: &TheorySetup) -> f64 {
421 let TheorySetup { points: x, probs: p, n0, nodes } = s;
422 let (n0, lp) = (*n0, log_priors(p));
423 let mut acc = 0.0;
424 let mut terms = vec![0.0; x.len()];
425 for (i, xi) in x.iter().enumerate() {
426 if p[i] == 0.0 {
427 continue;
428 }
429 let mut e = 0.0;
430 for (w, a) in nodes {
431 for (j, xj) in x.iter().enumerate() {
432 terms[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
433 }
434 e += a * log_sum_exp(&terms);
435 }
436 acc += p[i] * e;
437 }
438 -acc / std::f64::consts::LN_2
439}
440
441/// Generalised mutual information (bits/symbol) of the bit-wise decoder of a Gray-labelled
442/// constellation over AWGN, by Gauss–Hermite quadrature (OptiCommPy `theoryGMI`).
443///
444/// `GMI = H(X) - Σ_k H(B_k | Y)` with the labels of the points (point `i` carries the
445/// bits of `i`); same channel conventions and parameters as [`mi`]. For uniform symbols
446/// it is `Σ_k I(B_k; Y)`, OptiCommPy's formula; with shaping the bits are dependent and
447/// `Σ_k I(B_k; Y)` can exceed the entropy of the source, while `H(X) - Σ_k H(B_k | Y)`
448/// is the rate achievable by bit-metric decoding (Böcherer 2014) and the limit of the
449/// Monte Carlo estimators (OptiCommPy's `monteCarloGMI` included).
450///
451/// G. Böcherer, "Achievable rates for shaped bit-metric decoding," arXiv:1410.8075,
452/// 2014; A. Alvarado, T. Fehenberger, B. Chen and F. M. J. Willems, *J. Lightw.
453/// Technol.*, 36(2), 424–439, 2018.
454///
455/// # Errors
456/// [`Error::Config`] for an invalid constellation or probabilities.
457pub fn gmi(kind: ConstellationKind, m: usize, snr_db: f64, px: Option<&[f64]>, points: usize) -> Result<f64> {
458 constellation_gmi(&Constellation::new(kind, m)?, snr_db, px, RateChannel::Awgn, points)
459}
460
461/// The kernel of [`gmi`].
462pub(crate) fn gmi_of(s: &TheorySetup) -> f64 {
463 // GMI = H(X) - Σ_k H(B_k | Y): with shaped (dependent) bits, the sum of the
464 // bit-wise mutual informations Σ_k I(B_k; Y) can exceed H(X) and is not achievable
465 let mut gmi = entropy(&s.probs);
466 // -log2 P(b_k = b_k(i) | y)
467 for h_cond in bit_expectations(s, |all, same| log_sum_exp(all) - same) {
468 gmi -= h_cond / std::f64::consts::LN_2;
469 }
470 gmi
471}
472
473/// For every bit position `k`, $\sum_{i} p_{i} E_{w} f(\ell, \ell_{k})$ with
474/// `ℓ[j] = ln p_j - (|x_i - x_j + w|² - |w|²)/N_0` (the log-metrics of $y = x_{i} + w$,
475/// up to a common term) and $\ell_{k}$ their log-sum over the points whose bit `k`
476/// equals that of `i`; the metrics of a noise sample are computed once for all bits.
477pub(crate) fn bit_expectations(s: &TheorySetup, f: impl Fn(&[f64], f64) -> f64) -> Vec<f64> {
478 let TheorySetup { points: x, probs: p, n0, nodes } = s;
479 let m = x.len();
480 let b = m.trailing_zeros() as usize;
481 let lp = log_priors(p);
482 let mut all = vec![0.0; m];
483 let mut out = vec![0.0; b];
484 let mut e = vec![0.0; b];
485 for (i, xi) in x.iter().enumerate() {
486 if p[i] == 0.0 {
487 continue;
488 }
489 e.fill(0.0);
490 for (w, a) in nodes {
491 for (j, xj) in x.iter().enumerate() {
492 all[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
493 }
494 for (k, ek) in e.iter_mut().enumerate() {
495 let shift = b - 1 - k;
496 let bi = (i >> shift) & 1;
497 let same = LabelHalves::new(&all, shift, Jacobian::Exact).log_sum(bi);
498 *ek += a * f(&all, same);
499 }
500 }
501 for (o, ek) in out.iter_mut().zip(&e) {
502 *o += p[i] * ek;
503 }
504 }
505 out
506}
507
508/// The probability that the phase angle between two vectors of the same SNR `esn0_lin`
509/// (linear, $E_{s}/N_{0}$ each) perturbed by independent Gaussian noise exceeds `psi`
510/// in absolute value, $0 \le \psi \le \pi$ (Pawula, Rice & Roberts, 1982, the integral
511/// form for equal SNRs):
512///
513/// $$
514/// P(|\Delta\theta| > \psi) = \frac{\sin \psi}{2 \pi} \int_{-\pi/2}^{\pi/2}
515/// \frac{\exp\left[-\gamma_{s} (1 - \cos \psi \cos t)\right]}{1 - \cos \psi \cos t} \\, dt,
516/// $$
517///
518/// evaluated by Simpson's rule with 2 000 intervals (the integrand is smooth). It is
519/// the symbol error rate of M-DPSK for $\psi = \pi/M$, and $\frac{1}{2} e^{-\gamma_{s}}$
520/// for $\psi = \pi/2$ (DBPSK).
521///
522/// # Examples
523/// ```
524/// use comnum_mapping::theory::differential_phase_tail;
525/// let g = 5.0;
526/// assert!((differential_phase_tail(std::f64::consts::FRAC_PI_2, g) - 0.5 * (-g).exp()).abs() < 1e-9);
527/// assert!((differential_phase_tail(0.0, g) - 1.0).abs() < 1e-9);
528/// assert!(differential_phase_tail(std::f64::consts::PI, g).abs() < 1e-15);
529/// ```
530pub fn differential_phase_tail(psi: f64, esn0_lin: f64) -> f64 {
531 let psi = psi.clamp(0.0, std::f64::consts::PI);
532 if psi == 0.0 {
533 return 1.0;
534 }
535 let (s, c) = psi.sin_cos();
536 let f = |t: f64| {
537 let d = 1.0 - c * t.cos();
538 (-esn0_lin * d).exp() / d
539 };
540 let half = std::f64::consts::FRAC_PI_2;
541 s / (2.0 * std::f64::consts::PI) * comnum_math::simpson(f, -half, half, 2000)
542}
543
544/// The terms of eq. (9) of Cho and Yoon for the bit `k` (1 = most significant) of
545/// Gray-mapped `side`-PAM: the pairs $(X, Y)$ of Table I, so that
546/// $P_{b}(k) = \frac{1}{I} \sum X \\, \mathrm{erfc}(Y \cdot \ldots)$, with
547/// $X = (-1)^{\lfloor i 2^{k-1}/I \rfloor} (2^{k-1} - \lfloor i 2^{k-1}/I + \frac{1}{2} \rfloor)$
548/// and $Y = 2i + 1$ for $i = 0 \dots (1 - 2^{-k}) I - 1$.
549fn cho_yoon_terms(side: usize, k: usize) -> impl Iterator<Item = (f64, usize)> {
550 let step = 1usize << (k - 1);
551 (0..side - side / (1 << k)).map(move |i| {
552 let sign = if (i * step / side) % 2 == 0 { 1.0 } else { -1.0 };
553 let weight = step as f64 - ((i * step) as f64 / side as f64 + 0.5).floor();
554 (sign * weight, 2 * i + 1)
555 })
556}
557
558/// $\sum_{k} P_{b}(k)$ over the $\log_{2} I$ bits of Gray-mapped $I$-PAM, eq. (9) of Cho
559/// and Yoon with $\mathrm{erfc}(Y a) = 2Q(Y x)$: `x` is the half distance between
560/// neighbouring levels over the noise standard deviation.
561fn gray_pam_bit_errors(side: usize, x: f64) -> f64 {
562 let levels = side.trailing_zeros() as usize;
563 (1..=levels)
564 .map(|k| cho_yoon_terms(side, k).map(|(w, y)| w * 2.0 * qfunc(y as f64 * x)).sum::<f64>() / side as f64)
565 .sum()
566}
567
568fn ensure_pam_side(side: usize, what: &str) -> Result<()> {
569 ensure(side >= 2 && side.is_power_of_two(), || {
570 format!("the exact Gray BER needs {what} a power of two ≥ 2, got {side}")
571 })
572}
573
574/// Exact BER of Gray-mapped `m`-PAM over AWGN (K. Cho and D. Yoon, "On the general BER
575/// expression of one- and two-dimensional amplitude modulations," *IEEE Trans. Commun.*
576/// 50(7), 1074–1080, 2002, eqs. (9)–(10)), as a function of $E_{b}/N_{0}$ in dB:
577///
578/// $$P_{b} = \frac{1}{\log_{2} I} \sum_{k=1}^{\log_{2} I} P_{b}(k)\\,, \qquad
579/// P_{b}(k) = \frac{1}{I} \sum_{i} X_{i,k} \\, \mathrm{erfc}\left((2i + 1)
580/// \sqrt{\frac{3 \log_{2} I \cdot E_{b}/N_{0}}{I^{2} - 1}}\right),$$
581///
582/// with the coefficients $X_{i,k}$ of Table I of the paper. Unlike the
583/// nearest-neighbour approximation of [`ber`], it tends to 1/2 at low SNR.
584///
585/// # Errors
586/// [`Error::Config`] unless `m` is a power of two ≥ 2.
587///
588/// # Examples
589/// ```
590/// use comnum_mapping::theory::ber_gray_pam;
591/// use comnum_math::{db2lin, qfunc};
592/// // 4-PAM: (3Q(x) + 2Q(3x) - Q(5x))/4 with x = √(4/5 · E_b/N_0)
593/// let x = (0.8 * db2lin(10.0)).sqrt();
594/// let want = (3.0 * qfunc(x) + 2.0 * qfunc(3.0 * x) - qfunc(5.0 * x)) / 4.0;
595/// assert!((ber_gray_pam(4, 10.0).unwrap() - want).abs() < 1e-15);
596/// ```
597pub fn ber_gray_pam(m: usize, ebn0_db: f64) -> Result<f64> {
598 ensure_pam_side(m, "an order")?;
599 let log2m = f64::from(m.trailing_zeros());
600 let x = (6.0 * log2m * db2lin(ebn0_db) / ((m * m) as f64 - 1.0)).sqrt();
601 Ok(gray_pam_bit_errors(m, x) / log2m)
602}
603
604/// Exact BER of Gray-mapped `mi × mj` rectangular QAM over AWGN, two Gray-mapped PAM
605/// of `mi` and `mj` levels in quadrature (Cho and Yoon 2002, eqs. (19)–(22)):
606///
607/// $$P_{b} = \frac{1}{\log_{2}(IJ)} \left(\sum_{k=1}^{\log_{2} I} P_{I}(k) +
608/// \sum_{l=1}^{\log_{2} J} P_{J}(l)\right),$$
609///
610/// the terms of [`ber_gray_pam`] with the argument
611/// $(2i + 1)\sqrt{3 \log_{2}(IJ) \\, E_{b}/N_{0} / (I^{2} + J^{2} - 2)}$.
612///
613/// # Errors
614/// [`Error::Config`] unless `mi` and `mj` are powers of two ≥ 2.
615///
616/// # Examples
617/// ```
618/// use comnum_mapping::theory::{ber_gray_qam, ber_gray_rect_qam};
619/// // a square QAM is the case I = J
620/// let (a, b) = (ber_gray_rect_qam(4, 4, 8.0).unwrap(), ber_gray_qam(16, 8.0).unwrap());
621/// assert!((a - b).abs() < 1e-15);
622/// // 32-QAM as 8 × 4 lies between 16- and 64-QAM
623/// let p32 = ber_gray_rect_qam(8, 4, 12.0).unwrap();
624/// assert!(p32 > ber_gray_qam(16, 12.0).unwrap() && p32 < ber_gray_qam(64, 12.0).unwrap());
625/// ```
626pub fn ber_gray_rect_qam(mi: usize, mj: usize, ebn0_db: f64) -> Result<f64> {
627 ensure_pam_side(mi, "both sides")?;
628 ensure_pam_side(mj, "both sides")?;
629 let log2m = f64::from(mi.trailing_zeros() + mj.trailing_zeros());
630 let x = (6.0 * log2m * db2lin(ebn0_db) / ((mi * mi + mj * mj) as f64 - 2.0)).sqrt();
631 Ok((gray_pam_bit_errors(mi, x) + gray_pam_bit_errors(mj, x)) / log2m)
632}
633
634/// Exact BER of Gray-mapped square `m`-QAM over AWGN (Cho and Yoon 2002, eq. (16)), as
635/// a function of `E_b/N_0` in dB: [`ber_gray_rect_qam`] with `I = J = √m`. Unlike the
636/// nearest-neighbour approximation of [`ber`], it tends to 1/2 at low SNR, the BER of
637/// random decisions; for 16-QAM, `P_b = (3Q(x) + 2Q(3x) - Q(5x))/4` with
638/// `x = √(E_s/(5N_0))`.
639///
640/// # Errors
641/// [`Error::Config`] for a non-square order.
642///
643/// # Examples
644/// ```
645/// use comnum_mapping::theory::{ber, ber_gray_qam};
646/// use comnum_mapping::constellation::ConstellationKind;
647/// let exact = ber_gray_qam(16, 10.0).unwrap();
648/// let approx = ber(ConstellationKind::Qam, 16, 10.0).unwrap();
649/// assert!(exact > approx && (exact / approx - 1.0).abs() < 0.03);
650/// ```
651pub fn ber_gray_qam(m: usize, ebn0_db: f64) -> Result<f64> {
652 let side = qam_side(m).unwrap_or(0);
653 ensure(side >= 2 && side.is_power_of_two(), || format!("the exact Gray BER needs a square QAM, got {m}"))?;
654 ber_gray_rect_qam(side, side, ebn0_db)
655}
656
657/// Probability density of the phase error $\theta = \arg r - \varphi$ of a carrier
658/// $r = A e^{j\varphi} + w$ in circular Gaussian noise, at the SNR
659/// $\rho = A^{2}/\mathbb{E}|w|^{2}$ (linear) (S. O. Rice, *Bell Syst. Tech. J.* 27(1),
660/// 1948; as eq. (8) of A. U. Ahmed, S. C. Thompson and J. R. Zeidler, "Threshold
661/// extending receiver structures for CE-OFDM," *Proc. IEEE MILCOM*, 2007):
662///
663/// $$p(\theta) = \frac{e^{-\rho}}{2\pi}\left[1 + \sqrt{4\pi\rho}\\,\cos\theta\\,
664/// e^{\rho\cos^{2}\theta}\\,\Phi\left(\sqrt{2\rho}\cos\theta\right)\right],
665/// \qquad \theta \in (-\pi, \pi],$$
666///
667/// $\Phi$ the standard normal distribution function. It is uniform at $\rho = 0$ and
668/// tends to $\mathcal{N}(0, 1/(2\rho))$ at high SNR; integrated over the decision
669/// sectors it gives the error rates of PSK.
670///
671/// # Examples
672/// ```
673/// use comnum_mapping::theory::phase_pdf;
674/// // uniform without signal, Gaussian of variance 1/(2ρ) at high SNR
675/// assert!((phase_pdf(1.0, 0.0) - 1.0 / std::f64::consts::TAU).abs() < 1e-15);
676/// let rho = 400.0;
677/// let gauss = (rho / std::f64::consts::PI).sqrt();
678/// assert!((phase_pdf(0.0, rho) / gauss - 1.0).abs() < 1e-12);
679/// ```
680pub fn phase_pdf(theta: f64, snr_lin: f64) -> f64 {
681 let c = theta.cos();
682 let s2 = theta.sin().powi(2);
683 // e^{-ρ} √(4πρ) cos θ e^{ρ cos²θ} = √(4πρ) cos θ e^{-ρ sin²θ}, which does not overflow
684 (-snr_lin).exp() / std::f64::consts::TAU
685 + (snr_lin / std::f64::consts::PI).sqrt()
686 * c
687 * (-snr_lin * s2).exp()
688 * (1.0 - qfunc((2.0 * snr_lin).sqrt() * c))
689}
690
691/// The exact bit error rate of a Gray-labelled constellation over AWGN, as a function of
692/// $E_{b}/N_{0}$ (dB), where a closed form exists: [`ber_gray_pam`] for PAM,
693/// [`ber_gray_qam`] for square QAM (Cho and Yoon 2002), and [`ber`] for BPSK and QPSK,
694/// whose nearest-neighbour expression is exact.
695///
696/// # Errors
697/// [`Error::Config`] for another constellation (8-PSK and beyond, OOK, rectangular QAM:
698/// see [`ber`] and [`ber_gray_rect_qam`]).
699///
700/// # Examples
701/// ```
702/// use comnum_mapping::{theory, constellation::ConstellationKind};
703/// let qam = theory::ber_exact(ConstellationKind::Qam, 16, 10.0).unwrap();
704/// assert!((qam - theory::ber_gray_qam(16, 10.0).unwrap()).abs() < 1e-15);
705/// assert!(theory::ber_exact(ConstellationKind::Psk, 8, 10.0).is_err());
706/// ```
707pub fn ber_exact(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
708 match kind {
709 ConstellationKind::Pam => ber_gray_pam(m, ebn0_db),
710 ConstellationKind::Qam => ber_gray_qam(m, ebn0_db),
711 ConstellationKind::Psk if m <= 4 => ber(kind, m, ebn0_db),
712 _ => Err(Error::Config(format!("no exact Gray BER of {kind:?} of order {m}"))),
713 }
714}
715
716/// Bit error rate of Gray-labelled M-DPSK with noncoherent detection over AWGN, as a
717/// function of $E_{b}/N_{0}$ (dB): $\frac{1}{2} e^{-E_{b}/N_{0}}$ for DBPSK (Proakis &
718/// Salehi, §4.5), and for $M \ge 4$ the average number of wrong bits over the decision
719/// sectors of the differential phase, whose probabilities come from
720/// [`differential_phase_tail`] at $\gamma_{s} = E_{b}/N_{0} \log_{2} M$:
721///
722/// $$
723/// P_{b} = \frac{1}{\log_{2} M} \sum_{k=1}^{M-1} w_{k} \\,
724/// P\left(\Delta\theta \in \left[\frac{(2k - 1)\pi}{M}, \frac{(2k + 1)\pi}{M}\right)\right),
725/// $$
726///
727/// $w_{k}$ the Hamming weight of the Gray label of the transition $k$ (for DQPSK,
728/// $P_{b} = \frac{1}{2}[P(|\Delta\theta| > \pi/4) + P(|\Delta\theta| > 3\pi/4)]$).
729///
730/// # Errors
731/// [`Error::Config`] unless `m` is a power of two ≥ 2.
732///
733/// # Examples
734/// ```
735/// use comnum_mapping::theory::ber_dpsk;
736/// let g = comnum_math::db2lin(8.0);
737/// assert!((ber_dpsk(2, 8.0).unwrap() - 0.5 * (-g).exp()).abs() < 1e-12);
738/// // DQPSK is worse than DBPSK at the same E_b/N_0, and better than D8PSK
739/// assert!(ber_dpsk(4, 8.0).unwrap() > ber_dpsk(2, 8.0).unwrap());
740/// assert!(ber_dpsk(8, 8.0).unwrap() > ber_dpsk(4, 8.0).unwrap());
741/// ```
742pub fn ber_dpsk(m: usize, ebn0_db: f64) -> Result<f64> {
743 comnum_core::ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
744 let gb = db2lin(ebn0_db);
745 if m == 2 {
746 return Ok(0.5 * (-gb).exp());
747 }
748 let bits = m.trailing_zeros();
749 let gs = gb * f64::from(bits);
750 let pi = std::f64::consts::PI;
751 // P(Δθ < ψ) on (-π, π]
752 let cdf = |psi: f64| {
753 if psi < 0.0 { 0.5 * differential_phase_tail(-psi, gs) } else { 1.0 - 0.5 * differential_phase_tail(psi, gs) }
754 };
755 let gray = crate::constellation::gray_code(bits)?;
756 let mut errors = 0.0;
757 for k in 1..m {
758 let mut theta = 2.0 * pi * k as f64 / m as f64;
759 if theta > pi {
760 theta -= 2.0 * pi;
761 }
762 let (lo, hi) = (theta - pi / m as f64, theta + pi / m as f64);
763 let p = if hi > pi { 1.0 - cdf(lo) + cdf(hi - 2.0 * pi) } else { cdf(hi) - cdf(lo) };
764 errors += gray[k].count_ones() as f64 * p;
765 }
766 Ok(errors / f64::from(bits))
767}
768
769/// Bit error rate of DBPSK with coherent detection and differential decoding over AWGN
770/// (Proakis & Salehi, §3.2-2): an isolated symbol error spoils two differences, so
771/// $P_{b} = 2p(1 - p)$ with $p = Q(\sqrt{2 E_{b}/N_{0}})$.
772///
773/// # Examples
774/// ```
775/// use comnum_mapping::theory::ber_dbpsk_coherent;
776/// let p = comnum_math::qfunc((2.0 * comnum_math::db2lin(6.0)).sqrt());
777/// assert!((ber_dbpsk_coherent(6.0) - 2.0 * p * (1.0 - p)).abs() < 1e-15);
778/// ```
779pub fn ber_dbpsk_coherent(ebn0_db: f64) -> f64 {
780 let p = qfunc((2.0 * db2lin(ebn0_db)).sqrt());
781 2.0 * p * (1.0 - p)
782}
783
784/// Bit error rate of $M$-DPSK over AWGN with the detection `detection`: [`ber_dpsk`]
785/// for the noncoherent detection, [`ber_dbpsk_coherent`] for the coherent detection with
786/// differential decoding (defined for $M = 2$).
787///
788/// # Errors
789/// [`Error::Config`] for an order that is not a power of two ≥ 2, or the coherent
790/// detection with $M \ne 2$.
791///
792/// # Examples
793/// ```
794/// use comnum_mapping::differential::Detection;
795/// use comnum_mapping::theory::{ber_dbpsk_coherent, ber_differential, ber_dpsk};
796/// assert_eq!(ber_differential(4, 8.0, Detection::Noncoherent)?, ber_dpsk(4, 8.0)?);
797/// assert_eq!(ber_differential(2, 6.0, Detection::Coherent)?, ber_dbpsk_coherent(6.0));
798/// assert!(ber_differential(4, 6.0, Detection::Coherent).is_err());
799/// # Ok::<(), comnum_core::Error>(())
800/// ```
801pub fn ber_differential(m: usize, ebn0_db: f64, detection: crate::differential::Detection) -> Result<f64> {
802 use crate::differential::Detection;
803 match detection {
804 Detection::Noncoherent => ber_dpsk(m, ebn0_db),
805 Detection::Coherent if m == 2 => Ok(ber_dbpsk_coherent(ebn0_db)),
806 Detection::Coherent => Err(Error::Config(format!(
807 "coherent detection with differential decoding is defined for m = 2, got m = {m}"
808 ))),
809 }
810}
811
812/// Symbol error rate of $M$ orthogonal signals of equal energy detected noncoherently
813/// over AWGN (noncoherent M-FSK, chirp spread spectrum), as a function of $E_{s}/N_{0}$
814/// (dB): the magnitude of the matched filter of the sent signal is Rice of location
815/// $v = \sqrt{2 E_{s}/N_{0}}$, the $M - 1$ others Rayleigh, all of unit scale, and the
816/// largest wins (Proakis & Salehi, §4.5; Afisiadis et al. 2020, eq. (16)):
817///
818/// $$
819/// P_{s} = \int_{0}^{\infty} y\, e^{-(y^{2} + v^{2})/2} I_{0}(v y)
820/// \left[1 - \left(1 - e^{-y^{2}/2}\right)^{M - 1}\right] dy,
821/// $$
822///
823/// by Simpson's rule on $v \pm 12$, which avoids the cancellation of the alternating
824/// binomial sum for large $M$.
825///
826/// # Errors
827/// [`Error::Config`] if `m` is below 2.
828///
829/// # Examples
830/// ```
831/// use comnum_mapping::theory::ser_orthogonal_noncoherent;
832/// // binary: e^{-Es/(2 N0)} / 2
833/// let g = comnum_math::db2lin(9.0);
834/// assert!((ser_orthogonal_noncoherent(2, 9.0)? - 0.5 * (-g / 2.0).exp()).abs() < 1e-9);
835/// # Ok::<(), comnum_core::Error>(())
836/// ```
837pub fn ser_orthogonal_noncoherent(m: usize, esn0_db: f64) -> Result<f64> {
838 ensure(m >= 2, || format!("M orthogonal signals need M ≥ 2, got {m}"))?;
839 // 1 - (1 - e^{-y²/2})^n without cancellation
840 let one_minus_power = |y: f64, n: f64| {
841 let p = (-y * y / 2.0).exp();
842 if p >= 1.0 { 1.0 } else { -((n * (-p).ln_1p()).exp_m1()) }
843 };
844 let v = (2.0 * db2lin(esn0_db)).sqrt();
845 let rice = |y: f64| y * (-(y - v) * (y - v) / 2.0).exp() * bessel_i0e(y * v);
846 let (lo, hi) = ((v - 12.0).max(0.0), v + 12.0);
847 let others = (m - 1) as f64;
848 Ok(comnum_math::simpson(|y| rice(y) * one_minus_power(y, others), lo, hi, 4800).clamp(0.0, 1.0))
849}
850
851/// Bit error rate of $M$ orthogonal signals detected noncoherently over AWGN, as a
852/// function of $E_{s}/N_{0}$ (dB): $P_{b} = P_{s} M / (2(M - 1))$, every wrong symbol
853/// being equally likely ([`ser_orthogonal_noncoherent`]; Proakis & Salehi, §4.5).
854///
855/// # Errors
856/// As [`ser_orthogonal_noncoherent`].
857pub fn ber_orthogonal_noncoherent(m: usize, esn0_db: f64) -> Result<f64> {
858 let mf = m as f64;
859 Ok(ser_orthogonal_noncoherent(m, esn0_db)? * mf / (2.0 * (mf - 1.0)))
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865
866 #[test]
867 fn the_ser_of_a_constant_snr_is_that_of_awgn() {
868 // the SERs of BPSK and square QAM are exact (below 10⁻¹² the 1 - (1 - p)² of
869 // ser_esn0 loses digits)
870 for (kind, m) in [(ConstellationKind::Psk, 2), (ConstellationKind::Qam, 4), (ConstellationKind::Qam, 64)] {
871 for es_db in [0.0, 6.0, 12.0] {
872 let g = db2lin(es_db);
873 let by_mgf = ser_mgf(kind, m, |s| (s * g).exp()).unwrap();
874 let awgn = ser_esn0(kind, m, es_db).unwrap();
875 assert!((by_mgf / awgn - 1.0).abs() < 1e-9, "{kind:?} {m} at {es_db} dB: {by_mgf} against {awgn}");
876 }
877 }
878 // that of 8-PSK is the bound 2 Q(·), tight at high SNR
879 for (es_db, rel) in [(0.0, 0.05), (18.0, 1e-6)] {
880 let by_mgf = ser_mgf(ConstellationKind::Psk, 8, |s| (s * db2lin(es_db)).exp()).unwrap();
881 let bound = ser_esn0(ConstellationKind::Psk, 8, es_db).unwrap();
882 assert!(
883 by_mgf <= bound * (1.0 + 1e-12) && bound / by_mgf - 1.0 < rel,
884 "8-PSK at {es_db} dB: {by_mgf} against {bound}"
885 );
886 }
887 assert!(ser_mgf(ConstellationKind::Pam, 4, f64::exp).is_err());
888 }
889
890 /// The bitwise expectations as written before the shared halves: the log-sum of the
891 /// labels sharing the bit of `i`, by `log_sum_exp_iter`.
892 fn former_bit_expectations(s: &TheorySetup, f: impl Fn(&[f64], f64) -> f64) -> Vec<f64> {
893 let TheorySetup { points: x, probs: p, n0, nodes } = s;
894 let m = x.len();
895 let b = m.trailing_zeros() as usize;
896 let lp = log_priors(p);
897 let (mut all, mut out, mut e) = (vec![0.0; m], vec![0.0; b], vec![0.0; b]);
898 for (i, xi) in x.iter().enumerate() {
899 if p[i] == 0.0 {
900 continue;
901 }
902 e.fill(0.0);
903 for (w, a) in nodes {
904 for (j, xj) in x.iter().enumerate() {
905 all[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
906 }
907 for (k, ek) in e.iter_mut().enumerate() {
908 let shift = b - 1 - k;
909 let bi = (i >> shift) & 1;
910 let same =
911 comnum_math::log_sum_exp_iter((0..m).filter(|&j| (j >> shift) & 1 == bi).map(|j| all[j]));
912 *ek += a * f(&all, same);
913 }
914 }
915 for (o, ek) in out.iter_mut().zip(&e) {
916 *o += p[i] * ek;
917 }
918 }
919 out
920 }
921
922 #[test]
923 fn the_shared_halves_give_the_former_bitwise_expectations() {
924 let shaped = [0.4, 0.1, 0.3, 0.2, 0.05, 0.25, 0.15, 0.35];
925 for (c, px) in [
926 (Constellation::qam(16).unwrap(), None),
927 (Constellation::psk(8).unwrap(), Some(&shaped[..])),
928 (Constellation::pam(4).unwrap(), None),
929 ] {
930 for snr in [-5.0, 3.0, 15.0, 40.0] {
931 let s = setup(&c, px, snr, 12).unwrap();
932 let f = |all: &[f64], same: f64| log_sum_exp(all) - same;
933 let (new, old) = (bit_expectations(&s, f), former_bit_expectations(&s, f));
934 assert!(new.iter().zip(&old).all(|(a, b)| a.to_bits() == b.to_bits()), "{snr} dB");
935 }
936 }
937 }
938
939 #[test]
940 fn the_default_quadrature_has_converged() {
941 let c = Constellation::qam(16).unwrap();
942 for fading in [RateChannel::Awgn, RateChannel::Rayleigh] {
943 let at = |n| constellation_mi(&c, 10.0, None, fading, n).unwrap();
944 assert!((at(DEFAULT_QUADRATURE_POINTS) - at(80)).abs() < 1e-6, "{fading:?}");
945 }
946 }
947
948 #[test]
949 fn bpsk_theory_is_q_of_sqrt_2ebn0() {
950 for eb in [0.0, 4.0, 8.0] {
951 let g = db2lin(eb);
952 let q = qfunc((2.0 * g).sqrt());
953 assert!((ber(ConstellationKind::Psk, 2, eb).unwrap() / q - 1.0).abs() < 1e-9);
954 assert!((ser(ConstellationKind::Psk, 2, eb).unwrap() / q - 1.0).abs() < 1e-9);
955 assert!((ber(ConstellationKind::Qam, 4, eb).unwrap() / q - 1.0).abs() < 1e-9);
956 }
957 }
958
959 #[test]
960 fn closed_forms_reject_orders_they_do_not_cover() {
961 for (kind, m) in [
962 (ConstellationKind::Qam, 8),
963 (ConstellationKind::Qam, 32),
964 (ConstellationKind::Pam, 6),
965 (ConstellationKind::Psk, 1),
966 ] {
967 assert!(ber(kind, m, 10.0).is_err() && ser(kind, m, 10.0).is_err(), "{kind:?} {m}");
968 }
969 }
970
971 #[test]
972 fn dpsk_closed_forms() {
973 // the integral reproduces the closed form of DBPSK at every SNR
974 for g in [0.1f64, 1.0, 5.0, 20.0] {
975 let want = 0.5 * (-g).exp();
976 assert!((differential_phase_tail(std::f64::consts::FRAC_PI_2, g) / want - 1.0).abs() < 1e-8, "{g}");
977 }
978 // DQPSK: the sectors sum to one and the rate lies between QPSK and its double at 3 dB
979 // more (the classical loss of differential detection)
980 for eb in [4.0, 8.0, 12.0] {
981 let gs = 2.0 * db2lin(eb);
982 let t = |psi: f64| differential_phase_tail(psi, gs);
983 let want = 0.5 * (t(std::f64::consts::FRAC_PI_4) + t(3.0 * std::f64::consts::FRAC_PI_4));
984 // the sector sums cancel 1 - ½T, hence the rounding at 1e-12 of a 1e-5 rate
985 let got = ber_dpsk(4, eb).unwrap();
986 assert!((got / want - 1.0).abs() < 1e-9, "{eb}: {got:e} vs {want:e}");
987 let qpsk = ber(ConstellationKind::Psk, 4, eb).unwrap();
988 let qpsk_3db = ber(ConstellationKind::Psk, 4, eb - 3.0).unwrap();
989 assert!(want > qpsk && want < 2.0 * qpsk_3db, "{eb}: {want} against {qpsk} and {qpsk_3db}");
990 }
991 assert!(ber_dpsk(3, 1.0).is_err());
992 }
993
994 /// Exact error count of Gray-mapped `side`-PAM by enumeration of the decision
995 /// regions, with the labels of [`comnum_math::gray`]: `x` is the half distance between
996 /// levels over the noise standard deviation.
997 fn pam_bit_errors_by_enumeration(side: usize, x: f64) -> f64 {
998 let mut total = 0.0;
999 for j in 0..side {
1000 for r in 0..side {
1001 let d = r as f64 - j as f64;
1002 let lo = if r == 0 { 1.0 } else { qfunc((2.0 * d - 1.0) * x) };
1003 let hi = if r == side - 1 { 0.0 } else { qfunc((2.0 * d + 1.0) * x) };
1004 let wrong = (comnum_math::gray(j) ^ comnum_math::gray(r)).count_ones();
1005 total += f64::from(wrong) * (lo - hi);
1006 }
1007 }
1008 total / side as f64
1009 }
1010
1011 #[test]
1012 fn the_cho_yoon_sums_count_the_errors_of_the_gray_labels() {
1013 for side in [2usize, 4, 8, 16, 32, 64] {
1014 for x in [0.05, 0.3, 1.0, 2.5] {
1015 let (a, b) = (gray_pam_bit_errors(side, x), pam_bit_errors_by_enumeration(side, x));
1016 assert!((a - b).abs() < 1e-13, "{side}-PAM at x = {x}: {a} vs {b}");
1017 }
1018 }
1019 // rectangular QAM: two PAM in quadrature at the same distance
1020 for (mi, mj) in [(4usize, 2usize), (8, 4), (16, 8), (32, 16)] {
1021 let eb = 9.0;
1022 let log2m = f64::from(mi.trailing_zeros() + mj.trailing_zeros());
1023 let x = (6.0 * log2m * db2lin(eb) / ((mi * mi + mj * mj) as f64 - 2.0)).sqrt();
1024 let want = (pam_bit_errors_by_enumeration(mi, x) + pam_bit_errors_by_enumeration(mj, x)) / log2m;
1025 assert!((ber_gray_rect_qam(mi, mj, eb).unwrap() - want).abs() < 1e-14, "{mi} × {mj}");
1026 }
1027 // random decisions at low SNR, BPSK and QPSK
1028 assert!((ber_gray_pam(16, -60.0).unwrap() - 0.5).abs() < 1e-3);
1029 let q = qfunc((2.0 * db2lin(5.0)).sqrt());
1030 assert!((ber_gray_pam(2, 5.0).unwrap() - q).abs() < 1e-15);
1031 assert!((ber_gray_qam(4, 5.0).unwrap() - q).abs() < 1e-15);
1032 assert!(ber_gray_pam(6, 5.0).is_err() && ber_gray_rect_qam(8, 1, 5.0).is_err());
1033 }
1034
1035 #[test]
1036 fn the_phase_density_integrates_to_the_psk_error_rates() {
1037 let integral = |a: f64, b: f64, rho: f64| {
1038 let n = 20_000;
1039 let h = (b - a) / n as f64;
1040 (0..n).map(|i| phase_pdf(a + (i as f64 + 0.5) * h, rho)).sum::<f64>() * h
1041 };
1042 let pi = std::f64::consts::PI;
1043 for rho in [0.1, 1.0, 4.0, 20.0] {
1044 // to the accuracy of the midpoint rule
1045 assert!((integral(-pi, pi, rho) - 1.0).abs() < 1e-6, "{rho}: {}", integral(-pi, pi, rho));
1046 // BPSK: an error is Re r < 0, Q(√(2ρ)); QPSK: 2Q(√ρ) - Q(√ρ)²
1047 let bpsk = 2.0 * integral(pi / 2.0, pi, rho);
1048 assert!((bpsk - qfunc((2.0 * rho).sqrt())).abs() < 1e-6, "{rho}: {bpsk}");
1049 let q = qfunc(rho.sqrt());
1050 let qpsk = 2.0 * integral(pi / 4.0, pi, rho);
1051 assert!((qpsk - (2.0 * q - q * q)).abs() < 1e-6, "{rho}: {qpsk}");
1052 }
1053 }
1054}
1055
1056#[cfg(test)]
1057mod literature {
1058 use super::*;
1059
1060 /// K. Cho and D. Yoon, "On the general BER expression of one- and two-dimensional
1061 /// amplitude modulations," IEEE Trans. Commun. 50(7), 2002, Table I (p. 1076): the
1062 /// coefficients $[X_{I}(k)]$ and $[Y_{I}(k)]$ of eq. (9) for $I$ = 2, 4, 8 and 16.
1063 #[test]
1064 fn cho_2002_table_1() {
1065 let table: [(usize, usize, &[f64]); 10] = [
1066 (2, 1, &[1.0]),
1067 (4, 1, &[1.0, 1.0]),
1068 (4, 2, &[2.0, 1.0, -1.0]),
1069 (8, 1, &[1.0; 4]),
1070 (8, 2, &[2.0, 2.0, 1.0, 1.0, -1.0, -1.0]),
1071 (8, 3, &[4.0, 3.0, -3.0, -2.0, 2.0, 1.0, -1.0]),
1072 (16, 1, &[1.0; 8]),
1073 (16, 2, &[2.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0]),
1074 (16, 3, &[4.0, 4.0, 3.0, 3.0, -3.0, -3.0, -2.0, -2.0, 2.0, 2.0, 1.0, 1.0, -1.0, -1.0]),
1075 (16, 4, &[8.0, 7.0, -7.0, -6.0, 6.0, 5.0, -5.0, -4.0, 4.0, 3.0, -3.0, -2.0, 2.0, 1.0, -1.0]),
1076 ];
1077 for (side, k, xs) in table {
1078 let (x, y): (Vec<f64>, Vec<usize>) = cho_yoon_terms(side, k).unzip();
1079 assert_eq!(x, xs, "X_{side}({k})");
1080 // Y is 1, 3, 5, …, 2[(1 - 2^-k) I - 1] + 1
1081 assert_eq!(y, (0..xs.len()).map(|i| 2 * i + 1).collect::<Vec<_>>(), "Y_{side}({k})");
1082 }
1083 }
1084
1085 /// Cho and Yoon 2002, Sec. II-C and Fig. 2: "an additional 4–5 dB of SNR is
1086 /// required to transmit an extra bit to maintain an average BER of 1e-3". The exact
1087 /// BER gives 4.25, 4.62 and 4.88 dB from 4- to 32-PAM; the step is 3.73 dB from 2- to
1088 /// 4-PAM and grows to 5.06 and 5.20 dB beyond (64- and 128-PAM).
1089 #[test]
1090 fn cho_2002_fig_2_cost_of_an_extra_bit() {
1091 let required = |m: usize| bisect(|e| ber_gray_pam(m, e).unwrap() > 1e-3, -5.0, 60.0, 100);
1092 let r: Vec<f64> = (1..=7).map(|b| required(1 << b)).collect();
1093 assert!((r[0] - 6.79).abs() < 0.01, "BPSK at 1e-3: {}", r[0]);
1094 let steps: Vec<f64> = r.windows(2).map(|w| w[1] - w[0]).collect();
1095 for (i, s) in steps.iter().enumerate() {
1096 assert!((3.6..5.3).contains(s), "step {i}: {s}");
1097 }
1098 assert!(steps[1..4].iter().all(|s| (4.0..5.0).contains(s)), "{steps:?}");
1099 }
1100}