use crate::constellation::{Constellation, ConstellationKind, log_priors, qam_side};
use crate::demapper::LabelHalves;
use comnum_core::{Error, Result, ensure};
use comnum_math::{C64, Jacobian, bessel_i0e, bisect, db2lin, entropy, log_sum_exp, qfunc};
fn ensure_order(kind: ConstellationKind, m: usize) -> Result<()> {
ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
ensure(kind != ConstellationKind::Qam || qam_side(m).is_some(), || {
format!("the closed forms of QAM need a square order, got {m}")
})
}
pub fn ber(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
ensure_order(kind, m)?;
let g = db2lin(ebn0_db);
let mf = m as f64;
let k = mf.log2();
match kind {
ConstellationKind::Qam => {
let l = mf.sqrt();
Ok(2.0 * (1.0 - 1.0 / l) / l.log2() * qfunc((3.0 * l.log2() / (l * l - 1.0) * 2.0 * g).sqrt()))
}
ConstellationKind::Psk if m == 2 => Ok(qfunc((2.0 * g).sqrt())),
ConstellationKind::Psk => Ok(2.0 * qfunc((2.0 * k * g).sqrt() * (std::f64::consts::PI / mf).sin()) / k),
ConstellationKind::Pam => Ok(2.0 * (mf - 1.0) / mf * qfunc((6.0 * k / (mf * mf - 1.0) * g).sqrt()) / k),
other => Err(Error::Config(format!("no theoretical BER for {other:?}"))),
}
}
pub fn ser(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
ensure_order(kind, m)?;
ser_at(kind, m, db2lin(ebn0_db) * (m as f64).log2())
}
pub fn ser_esn0(kind: ConstellationKind, m: usize, esn0_db: f64) -> Result<f64> {
ensure_order(kind, m)?;
ser_at(kind, m, db2lin(esn0_db))
}
fn ser_at(kind: ConstellationKind, m: usize, es: f64) -> Result<f64> {
let mf = m as f64;
match kind {
ConstellationKind::Qam => {
let l = mf.sqrt();
let p = 2.0 * (1.0 - 1.0 / l) * qfunc((3.0 * es / (mf - 1.0)).sqrt());
Ok(1.0 - (1.0 - p) * (1.0 - p))
}
ConstellationKind::Pam => Ok(2.0 * (mf - 1.0) / mf * qfunc((6.0 * es / (mf * mf - 1.0)).sqrt())),
ConstellationKind::Psk => {
let p = 2.0 * qfunc((2.0 * es).sqrt() * (std::f64::consts::PI / mf).sin());
Ok(if m == 2 { p / 2.0 } else { p })
}
other => Err(Error::Config(format!("no theoretical SER for {other:?}"))),
}
}
pub fn ser_mgf(kind: ConstellationKind, m: usize, mgf: impl Fn(f64) -> f64) -> Result<f64> {
ensure_order(kind, m)?;
let mf = m as f64;
let (x, w) = comnum_math::gauss_legendre(96);
let integral = |c: f64, b: f64| -> f64 {
x.iter().zip(&w).map(|(t, wt)| wt * mgf(-c / (b / 2.0 * (t + 1.0)).sin().powi(2))).sum::<f64>() * b / 2.0
};
match kind {
ConstellationKind::Psk => {
let g = (std::f64::consts::PI / mf).sin().powi(2);
Ok(integral(g, (mf - 1.0) * std::f64::consts::PI / mf) / std::f64::consts::PI)
}
ConstellationKind::Qam => {
let (c, q) = (1.5 / (mf - 1.0), 1.0 - 1.0 / mf.sqrt());
let pi = std::f64::consts::PI;
Ok(4.0 * q / pi * integral(c, pi / 2.0) - 4.0 * q * q / pi * integral(c, pi / 4.0))
}
other => Err(Error::Config(format!("no SER by the MGF for {other:?}"))),
}
}
pub fn ebn0_for_ber(kind: ConstellationKind, m: usize, target: f64) -> Result<f64> {
ebn0_for(|e| ber(kind, m, e), "BER", target)
}
pub fn ebn0_for_ser(kind: ConstellationKind, m: usize, target: f64) -> Result<f64> {
ebn0_for(|e| ser(kind, m, e), "SER", target)
}
fn ebn0_for(rate: impl Fn(f64) -> Result<f64>, what: &str, target: f64) -> Result<f64> {
let (a, b) = (-10.0, 60.0);
ensure(rate(a)? > target && target > rate(b)?, || format!("no Eb/N0 in [-10, 60] dB gives a {what} of {target}"))?;
Ok(bisect(|e| rate(e).is_ok_and(|v| v > target), a, b, 200))
}
pub fn ber_rayleigh_mrc(mean_ebn0_lin: f64, branches: usize) -> Result<f64> {
ensure(branches >= 1, || "maximal-ratio combining needs at least one branch".into())?;
let mu = (mean_ebn0_lin / (1.0 + mean_ebn0_lin)).sqrt();
let l = branches;
let mut binom = 1.0;
let mut sum = 0.0;
for k in 0..l {
if k > 0 {
binom *= (l - 1 + k) as f64 / k as f64;
}
sum += binom * ((1.0 + mu) / 2.0).powi(k as i32);
}
Ok(((1.0 - mu) / 2.0).powi(l as i32) * sum)
}
pub(crate) struct TheorySetup {
pub(crate) points: Vec<C64>,
pub(crate) probs: Vec<f64>,
pub(crate) n0: f64,
pub(crate) nodes: Vec<(C64, f64)>,
}
pub(crate) fn setup(c: &Constellation, px: Option<&[f64]>, snr_db: f64, points: usize) -> Result<TheorySetup> {
let p = c.priors(px)?;
let pts = c.normalized_under(&p)?.points().to_vec();
let real = c.kind().is_real();
let sigma2 = db2lin(-snr_db);
let n0 = if real { 2.0 * sigma2 } else { sigma2 };
Ok(TheorySetup { points: pts, probs: p, n0, nodes: comnum_math::gaussian_nodes(sigma2, !real, points) })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RateChannel {
Awgn,
Rayleigh,
}
impl std::str::FromStr for RateChannel {
type Err = Error;
fn from_str(name: &str) -> Result<RateChannel> {
match name.to_ascii_lowercase().as_str() {
"awgn" => Ok(RateChannel::Awgn),
"rayleigh" => Ok(RateChannel::Rayleigh),
other => Err(Error::Config(format!("unknown fading {other:?}: awgn, rayleigh"))),
}
}
}
pub(crate) fn over_fading(f: impl Fn(f64) -> Result<f64>, snr_db: f64, fading: RateChannel) -> Result<f64> {
if fading == RateChannel::Awgn {
return f(snr_db);
}
let lo = (1e-4 / db2lin(snr_db).max(1.0)).ln();
let hi = 40f64.ln();
let n = (8.0 * (hi - lo)).ceil() as usize;
let at = |u: f64| -> Result<f64> { Ok((u - u.exp()).exp() * f(snr_db + 10.0 * u / std::f64::consts::LN_10)?) };
let tail = -(-lo.exp()).exp_m1() * f(snr_db + 10.0 * lo / std::f64::consts::LN_10)?;
Ok(comnum_math::try_simpson(at, lo, hi, n)? + tail)
}
pub const DEFAULT_QUADRATURE_POINTS: usize = 40;
pub fn constellation_mi(
c: &Constellation,
snr_db: f64,
px: Option<&[f64]>,
fading: RateChannel,
points: usize,
) -> Result<f64> {
over_fading(|s| Ok(mi_of(&setup(c, px, s, points)?)), snr_db, fading)
}
pub fn constellation_gmi(
c: &Constellation,
snr_db: f64,
px: Option<&[f64]>,
fading: RateChannel,
points: usize,
) -> Result<f64> {
over_fading(|s| Ok(gmi_of(&setup(c, px, s, points)?)), snr_db, fading)
}
pub fn mi(kind: ConstellationKind, m: usize, snr_db: f64, px: Option<&[f64]>, points: usize) -> Result<f64> {
constellation_mi(&Constellation::new(kind, m)?, snr_db, px, RateChannel::Awgn, points)
}
pub(crate) fn mi_of(s: &TheorySetup) -> f64 {
let TheorySetup { points: x, probs: p, n0, nodes } = s;
let (n0, lp) = (*n0, log_priors(p));
let mut acc = 0.0;
let mut terms = vec![0.0; x.len()];
for (i, xi) in x.iter().enumerate() {
if p[i] == 0.0 {
continue;
}
let mut e = 0.0;
for (w, a) in nodes {
for (j, xj) in x.iter().enumerate() {
terms[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
}
e += a * log_sum_exp(&terms);
}
acc += p[i] * e;
}
-acc / std::f64::consts::LN_2
}
pub fn gmi(kind: ConstellationKind, m: usize, snr_db: f64, px: Option<&[f64]>, points: usize) -> Result<f64> {
constellation_gmi(&Constellation::new(kind, m)?, snr_db, px, RateChannel::Awgn, points)
}
pub(crate) fn gmi_of(s: &TheorySetup) -> f64 {
let mut gmi = entropy(&s.probs);
for h_cond in bit_expectations(s, |all, same| log_sum_exp(all) - same) {
gmi -= h_cond / std::f64::consts::LN_2;
}
gmi
}
pub(crate) fn bit_expectations(s: &TheorySetup, f: impl Fn(&[f64], f64) -> f64) -> Vec<f64> {
let TheorySetup { points: x, probs: p, n0, nodes } = s;
let m = x.len();
let b = m.trailing_zeros() as usize;
let lp = log_priors(p);
let mut all = vec![0.0; m];
let mut out = vec![0.0; b];
let mut e = vec![0.0; b];
for (i, xi) in x.iter().enumerate() {
if p[i] == 0.0 {
continue;
}
e.fill(0.0);
for (w, a) in nodes {
for (j, xj) in x.iter().enumerate() {
all[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
}
for (k, ek) in e.iter_mut().enumerate() {
let shift = b - 1 - k;
let bi = (i >> shift) & 1;
let same = LabelHalves::new(&all, shift, Jacobian::Exact).log_sum(bi);
*ek += a * f(&all, same);
}
}
for (o, ek) in out.iter_mut().zip(&e) {
*o += p[i] * ek;
}
}
out
}
pub fn differential_phase_tail(psi: f64, esn0_lin: f64) -> f64 {
let psi = psi.clamp(0.0, std::f64::consts::PI);
if psi == 0.0 {
return 1.0;
}
let (s, c) = psi.sin_cos();
let f = |t: f64| {
let d = 1.0 - c * t.cos();
(-esn0_lin * d).exp() / d
};
let half = std::f64::consts::FRAC_PI_2;
s / (2.0 * std::f64::consts::PI) * comnum_math::simpson(f, -half, half, 2000)
}
fn cho_yoon_terms(side: usize, k: usize) -> impl Iterator<Item = (f64, usize)> {
let step = 1usize << (k - 1);
(0..side - side / (1 << k)).map(move |i| {
let sign = if (i * step / side) % 2 == 0 { 1.0 } else { -1.0 };
let weight = step as f64 - ((i * step) as f64 / side as f64 + 0.5).floor();
(sign * weight, 2 * i + 1)
})
}
fn gray_pam_bit_errors(side: usize, x: f64) -> f64 {
let levels = side.trailing_zeros() as usize;
(1..=levels)
.map(|k| cho_yoon_terms(side, k).map(|(w, y)| w * 2.0 * qfunc(y as f64 * x)).sum::<f64>() / side as f64)
.sum()
}
fn ensure_pam_side(side: usize, what: &str) -> Result<()> {
ensure(side >= 2 && side.is_power_of_two(), || {
format!("the exact Gray BER needs {what} a power of two ≥ 2, got {side}")
})
}
pub fn ber_gray_pam(m: usize, ebn0_db: f64) -> Result<f64> {
ensure_pam_side(m, "an order")?;
let log2m = f64::from(m.trailing_zeros());
let x = (6.0 * log2m * db2lin(ebn0_db) / ((m * m) as f64 - 1.0)).sqrt();
Ok(gray_pam_bit_errors(m, x) / log2m)
}
pub fn ber_gray_rect_qam(mi: usize, mj: usize, ebn0_db: f64) -> Result<f64> {
ensure_pam_side(mi, "both sides")?;
ensure_pam_side(mj, "both sides")?;
let log2m = f64::from(mi.trailing_zeros() + mj.trailing_zeros());
let x = (6.0 * log2m * db2lin(ebn0_db) / ((mi * mi + mj * mj) as f64 - 2.0)).sqrt();
Ok((gray_pam_bit_errors(mi, x) + gray_pam_bit_errors(mj, x)) / log2m)
}
pub fn ber_gray_qam(m: usize, ebn0_db: f64) -> Result<f64> {
let side = qam_side(m).unwrap_or(0);
ensure(side >= 2 && side.is_power_of_two(), || format!("the exact Gray BER needs a square QAM, got {m}"))?;
ber_gray_rect_qam(side, side, ebn0_db)
}
pub fn phase_pdf(theta: f64, snr_lin: f64) -> f64 {
let c = theta.cos();
let s2 = theta.sin().powi(2);
(-snr_lin).exp() / std::f64::consts::TAU
+ (snr_lin / std::f64::consts::PI).sqrt()
* c
* (-snr_lin * s2).exp()
* (1.0 - qfunc((2.0 * snr_lin).sqrt() * c))
}
pub fn ber_exact(kind: ConstellationKind, m: usize, ebn0_db: f64) -> Result<f64> {
match kind {
ConstellationKind::Pam => ber_gray_pam(m, ebn0_db),
ConstellationKind::Qam => ber_gray_qam(m, ebn0_db),
ConstellationKind::Psk if m <= 4 => ber(kind, m, ebn0_db),
_ => Err(Error::Config(format!("no exact Gray BER of {kind:?} of order {m}"))),
}
}
pub fn ber_dpsk(m: usize, ebn0_db: f64) -> Result<f64> {
comnum_core::ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
let gb = db2lin(ebn0_db);
if m == 2 {
return Ok(0.5 * (-gb).exp());
}
let bits = m.trailing_zeros();
let gs = gb * f64::from(bits);
let pi = std::f64::consts::PI;
let cdf = |psi: f64| {
if psi < 0.0 { 0.5 * differential_phase_tail(-psi, gs) } else { 1.0 - 0.5 * differential_phase_tail(psi, gs) }
};
let gray = crate::constellation::gray_code(bits)?;
let mut errors = 0.0;
for k in 1..m {
let mut theta = 2.0 * pi * k as f64 / m as f64;
if theta > pi {
theta -= 2.0 * pi;
}
let (lo, hi) = (theta - pi / m as f64, theta + pi / m as f64);
let p = if hi > pi { 1.0 - cdf(lo) + cdf(hi - 2.0 * pi) } else { cdf(hi) - cdf(lo) };
errors += gray[k].count_ones() as f64 * p;
}
Ok(errors / f64::from(bits))
}
pub fn ber_dbpsk_coherent(ebn0_db: f64) -> f64 {
let p = qfunc((2.0 * db2lin(ebn0_db)).sqrt());
2.0 * p * (1.0 - p)
}
pub fn ber_differential(m: usize, ebn0_db: f64, detection: crate::differential::Detection) -> Result<f64> {
use crate::differential::Detection;
match detection {
Detection::Noncoherent => ber_dpsk(m, ebn0_db),
Detection::Coherent if m == 2 => Ok(ber_dbpsk_coherent(ebn0_db)),
Detection::Coherent => Err(Error::Config(format!(
"coherent detection with differential decoding is defined for m = 2, got m = {m}"
))),
}
}
pub fn ser_orthogonal_noncoherent(m: usize, esn0_db: f64) -> Result<f64> {
ensure(m >= 2, || format!("M orthogonal signals need M ≥ 2, got {m}"))?;
let one_minus_power = |y: f64, n: f64| {
let p = (-y * y / 2.0).exp();
if p >= 1.0 { 1.0 } else { -((n * (-p).ln_1p()).exp_m1()) }
};
let v = (2.0 * db2lin(esn0_db)).sqrt();
let rice = |y: f64| y * (-(y - v) * (y - v) / 2.0).exp() * bessel_i0e(y * v);
let (lo, hi) = ((v - 12.0).max(0.0), v + 12.0);
let others = (m - 1) as f64;
Ok(comnum_math::simpson(|y| rice(y) * one_minus_power(y, others), lo, hi, 4800).clamp(0.0, 1.0))
}
pub fn ber_orthogonal_noncoherent(m: usize, esn0_db: f64) -> Result<f64> {
let mf = m as f64;
Ok(ser_orthogonal_noncoherent(m, esn0_db)? * mf / (2.0 * (mf - 1.0)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_ser_of_a_constant_snr_is_that_of_awgn() {
for (kind, m) in [(ConstellationKind::Psk, 2), (ConstellationKind::Qam, 4), (ConstellationKind::Qam, 64)] {
for es_db in [0.0, 6.0, 12.0] {
let g = db2lin(es_db);
let by_mgf = ser_mgf(kind, m, |s| (s * g).exp()).unwrap();
let awgn = ser_esn0(kind, m, es_db).unwrap();
assert!((by_mgf / awgn - 1.0).abs() < 1e-9, "{kind:?} {m} at {es_db} dB: {by_mgf} against {awgn}");
}
}
for (es_db, rel) in [(0.0, 0.05), (18.0, 1e-6)] {
let by_mgf = ser_mgf(ConstellationKind::Psk, 8, |s| (s * db2lin(es_db)).exp()).unwrap();
let bound = ser_esn0(ConstellationKind::Psk, 8, es_db).unwrap();
assert!(
by_mgf <= bound * (1.0 + 1e-12) && bound / by_mgf - 1.0 < rel,
"8-PSK at {es_db} dB: {by_mgf} against {bound}"
);
}
assert!(ser_mgf(ConstellationKind::Pam, 4, f64::exp).is_err());
}
fn former_bit_expectations(s: &TheorySetup, f: impl Fn(&[f64], f64) -> f64) -> Vec<f64> {
let TheorySetup { points: x, probs: p, n0, nodes } = s;
let m = x.len();
let b = m.trailing_zeros() as usize;
let lp = log_priors(p);
let (mut all, mut out, mut e) = (vec![0.0; m], vec![0.0; b], vec![0.0; b]);
for (i, xi) in x.iter().enumerate() {
if p[i] == 0.0 {
continue;
}
e.fill(0.0);
for (w, a) in nodes {
for (j, xj) in x.iter().enumerate() {
all[j] = lp[j] - ((xi - xj + w).norm_sqr() - w.norm_sqr()) / n0;
}
for (k, ek) in e.iter_mut().enumerate() {
let shift = b - 1 - k;
let bi = (i >> shift) & 1;
let same =
comnum_math::log_sum_exp_iter((0..m).filter(|&j| (j >> shift) & 1 == bi).map(|j| all[j]));
*ek += a * f(&all, same);
}
}
for (o, ek) in out.iter_mut().zip(&e) {
*o += p[i] * ek;
}
}
out
}
#[test]
fn the_shared_halves_give_the_former_bitwise_expectations() {
let shaped = [0.4, 0.1, 0.3, 0.2, 0.05, 0.25, 0.15, 0.35];
for (c, px) in [
(Constellation::qam(16).unwrap(), None),
(Constellation::psk(8).unwrap(), Some(&shaped[..])),
(Constellation::pam(4).unwrap(), None),
] {
for snr in [-5.0, 3.0, 15.0, 40.0] {
let s = setup(&c, px, snr, 12).unwrap();
let f = |all: &[f64], same: f64| log_sum_exp(all) - same;
let (new, old) = (bit_expectations(&s, f), former_bit_expectations(&s, f));
assert!(new.iter().zip(&old).all(|(a, b)| a.to_bits() == b.to_bits()), "{snr} dB");
}
}
}
#[test]
fn the_default_quadrature_has_converged() {
let c = Constellation::qam(16).unwrap();
for fading in [RateChannel::Awgn, RateChannel::Rayleigh] {
let at = |n| constellation_mi(&c, 10.0, None, fading, n).unwrap();
assert!((at(DEFAULT_QUADRATURE_POINTS) - at(80)).abs() < 1e-6, "{fading:?}");
}
}
#[test]
fn bpsk_theory_is_q_of_sqrt_2ebn0() {
for eb in [0.0, 4.0, 8.0] {
let g = db2lin(eb);
let q = qfunc((2.0 * g).sqrt());
assert!((ber(ConstellationKind::Psk, 2, eb).unwrap() / q - 1.0).abs() < 1e-9);
assert!((ser(ConstellationKind::Psk, 2, eb).unwrap() / q - 1.0).abs() < 1e-9);
assert!((ber(ConstellationKind::Qam, 4, eb).unwrap() / q - 1.0).abs() < 1e-9);
}
}
#[test]
fn closed_forms_reject_orders_they_do_not_cover() {
for (kind, m) in [
(ConstellationKind::Qam, 8),
(ConstellationKind::Qam, 32),
(ConstellationKind::Pam, 6),
(ConstellationKind::Psk, 1),
] {
assert!(ber(kind, m, 10.0).is_err() && ser(kind, m, 10.0).is_err(), "{kind:?} {m}");
}
}
#[test]
fn dpsk_closed_forms() {
for g in [0.1f64, 1.0, 5.0, 20.0] {
let want = 0.5 * (-g).exp();
assert!((differential_phase_tail(std::f64::consts::FRAC_PI_2, g) / want - 1.0).abs() < 1e-8, "{g}");
}
for eb in [4.0, 8.0, 12.0] {
let gs = 2.0 * db2lin(eb);
let t = |psi: f64| differential_phase_tail(psi, gs);
let want = 0.5 * (t(std::f64::consts::FRAC_PI_4) + t(3.0 * std::f64::consts::FRAC_PI_4));
let got = ber_dpsk(4, eb).unwrap();
assert!((got / want - 1.0).abs() < 1e-9, "{eb}: {got:e} vs {want:e}");
let qpsk = ber(ConstellationKind::Psk, 4, eb).unwrap();
let qpsk_3db = ber(ConstellationKind::Psk, 4, eb - 3.0).unwrap();
assert!(want > qpsk && want < 2.0 * qpsk_3db, "{eb}: {want} against {qpsk} and {qpsk_3db}");
}
assert!(ber_dpsk(3, 1.0).is_err());
}
fn pam_bit_errors_by_enumeration(side: usize, x: f64) -> f64 {
let mut total = 0.0;
for j in 0..side {
for r in 0..side {
let d = r as f64 - j as f64;
let lo = if r == 0 { 1.0 } else { qfunc((2.0 * d - 1.0) * x) };
let hi = if r == side - 1 { 0.0 } else { qfunc((2.0 * d + 1.0) * x) };
let wrong = (comnum_math::gray(j) ^ comnum_math::gray(r)).count_ones();
total += f64::from(wrong) * (lo - hi);
}
}
total / side as f64
}
#[test]
fn the_cho_yoon_sums_count_the_errors_of_the_gray_labels() {
for side in [2usize, 4, 8, 16, 32, 64] {
for x in [0.05, 0.3, 1.0, 2.5] {
let (a, b) = (gray_pam_bit_errors(side, x), pam_bit_errors_by_enumeration(side, x));
assert!((a - b).abs() < 1e-13, "{side}-PAM at x = {x}: {a} vs {b}");
}
}
for (mi, mj) in [(4usize, 2usize), (8, 4), (16, 8), (32, 16)] {
let eb = 9.0;
let log2m = f64::from(mi.trailing_zeros() + mj.trailing_zeros());
let x = (6.0 * log2m * db2lin(eb) / ((mi * mi + mj * mj) as f64 - 2.0)).sqrt();
let want = (pam_bit_errors_by_enumeration(mi, x) + pam_bit_errors_by_enumeration(mj, x)) / log2m;
assert!((ber_gray_rect_qam(mi, mj, eb).unwrap() - want).abs() < 1e-14, "{mi} × {mj}");
}
assert!((ber_gray_pam(16, -60.0).unwrap() - 0.5).abs() < 1e-3);
let q = qfunc((2.0 * db2lin(5.0)).sqrt());
assert!((ber_gray_pam(2, 5.0).unwrap() - q).abs() < 1e-15);
assert!((ber_gray_qam(4, 5.0).unwrap() - q).abs() < 1e-15);
assert!(ber_gray_pam(6, 5.0).is_err() && ber_gray_rect_qam(8, 1, 5.0).is_err());
}
#[test]
fn the_phase_density_integrates_to_the_psk_error_rates() {
let integral = |a: f64, b: f64, rho: f64| {
let n = 20_000;
let h = (b - a) / n as f64;
(0..n).map(|i| phase_pdf(a + (i as f64 + 0.5) * h, rho)).sum::<f64>() * h
};
let pi = std::f64::consts::PI;
for rho in [0.1, 1.0, 4.0, 20.0] {
assert!((integral(-pi, pi, rho) - 1.0).abs() < 1e-6, "{rho}: {}", integral(-pi, pi, rho));
let bpsk = 2.0 * integral(pi / 2.0, pi, rho);
assert!((bpsk - qfunc((2.0 * rho).sqrt())).abs() < 1e-6, "{rho}: {bpsk}");
let q = qfunc(rho.sqrt());
let qpsk = 2.0 * integral(pi / 4.0, pi, rho);
assert!((qpsk - (2.0 * q - q * q)).abs() < 1e-6, "{rho}: {qpsk}");
}
}
}
#[cfg(test)]
mod literature {
use super::*;
#[test]
fn cho_2002_table_1() {
let table: [(usize, usize, &[f64]); 10] = [
(2, 1, &[1.0]),
(4, 1, &[1.0, 1.0]),
(4, 2, &[2.0, 1.0, -1.0]),
(8, 1, &[1.0; 4]),
(8, 2, &[2.0, 2.0, 1.0, 1.0, -1.0, -1.0]),
(8, 3, &[4.0, 3.0, -3.0, -2.0, 2.0, 1.0, -1.0]),
(16, 1, &[1.0; 8]),
(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]),
(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]),
(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]),
];
for (side, k, xs) in table {
let (x, y): (Vec<f64>, Vec<usize>) = cho_yoon_terms(side, k).unzip();
assert_eq!(x, xs, "X_{side}({k})");
assert_eq!(y, (0..xs.len()).map(|i| 2 * i + 1).collect::<Vec<_>>(), "Y_{side}({k})");
}
}
#[test]
fn cho_2002_fig_2_cost_of_an_extra_bit() {
let required = |m: usize| bisect(|e| ber_gray_pam(m, e).unwrap() > 1e-3, -5.0, 60.0, 100);
let r: Vec<f64> = (1..=7).map(|b| required(1 << b)).collect();
assert!((r[0] - 6.79).abs() < 0.01, "BPSK at 1e-3: {}", r[0]);
let steps: Vec<f64> = r.windows(2).map(|w| w[1] - w[0]).collect();
for (i, s) in steps.iter().enumerate() {
assert!((3.6..5.3).contains(s), "step {i}: {s}");
}
assert!(steps[1..4].iter().all(|s| (4.0..5.0).contains(s)), "{steps:?}");
}
}