use crate::constellation::Constellation;
use crate::theory::{RateChannel, TheorySetup, bit_expectations, over_fading, setup};
use comnum_core::Result;
use comnum_math::{db2lin, log_sum_exp_iter};
pub fn cm_cutoff_rate(c: &Constellation, snr_db: f64, fading: RateChannel) -> f64 {
let x = c.normalized();
let x = x.points();
let m = x.len() as f64;
let scale = db2lin(snr_db) / if c.kind().is_real() { 8.0 } else { 4.0 };
let mut sum = 0.0;
for xi in x {
for xj in x {
let a = scale * (xi - xj).norm_sqr();
sum += match fading {
RateChannel::Awgn => (-a).exp(),
RateChannel::Rayleigh => 1.0 / (1.0 + a),
};
}
}
m.log2() - (sum / m).log2()
}
pub fn bicm_cutoff_rate(c: &Constellation, snr_db: f64, fading: RateChannel, points: usize) -> Result<f64> {
let b = c.bits_per_symbol() as f64;
let factor = over_fading(|s| Ok(bhattacharyya(&setup(c, None, s, points)?)), snr_db, fading)?;
Ok(b * (1.0 - factor.ln_1p() / std::f64::consts::LN_2))
}
fn bhattacharyya(s: &TheorySetup) -> f64 {
let terms = bit_expectations(s, |all, same| {
let total = log_sum_exp_iter(all.iter().copied());
let other = total + (-(same - total).exp()).ln_1p();
(0.5 * (other - same)).exp()
});
terms.iter().sum::<f64>() / terms.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constellation::ConstellationKind;
use crate::theory::{constellation_gmi, constellation_mi};
use comnum_core::rng::Rng;
#[test]
fn the_rayleigh_mean_of_the_cm_cutoff_rate_is_its_closed_form() {
let c = Constellation::qam(16).unwrap();
let m = 16f64;
for snr_db in [-5.0, 5.0, 20.0, 40.0] {
let inner = |s: f64| Ok((m.log2() - cm_cutoff_rate(&c, s, RateChannel::Awgn)).exp2());
let mean = over_fading(inner, snr_db, RateChannel::Rayleigh).unwrap();
let want = m.log2() - mean.log2();
let got = cm_cutoff_rate(&c, snr_db, RateChannel::Rayleigh);
assert!((got - want).abs() < 1e-6, "{snr_db} dB: {got} vs {want}");
}
}
#[test]
fn the_capacities_over_rayleigh_fading_match_a_simulation() {
let c = Constellation::qam(16).unwrap().normalized();
let x = c.points();
let snr_db = 8.0;
let sigma2 = db2lin(-snr_db);
let mut r = Rng::new(11);
let n = 40_000;
let (mut cm, mut bicm) = (Vec::with_capacity(n), Vec::with_capacity(n));
for _ in 0..n {
let i = r.below(16);
let g = r.complex_gaussian();
let y = g * x[i] + r.complex_gaussian() * sigma2.sqrt();
let l: Vec<f64> = x.iter().map(|z| -(y - g * z).norm_sqr() / sigma2).collect();
let all = log_sum_exp_iter(l.iter().copied());
cm.push(4.0 - (all - l[i]) / std::f64::consts::LN_2);
let mut v = 4.0;
for k in 0..4 {
let bit = (i >> (3 - k)) & 1;
let same = log_sum_exp_iter((0..16).filter(|&j| (j >> (3 - k)) & 1 == bit).map(|j| l[j]));
v -= (all - same) / std::f64::consts::LN_2;
}
bicm.push(v);
}
for (v, want) in [
(&cm, constellation_mi(&c, snr_db, None, RateChannel::Rayleigh, 16).unwrap()),
(&bicm, constellation_gmi(&c, snr_db, None, RateChannel::Rayleigh, 16).unwrap()),
] {
let mean = v.iter().sum::<f64>() / n as f64;
let sd = (v.iter().map(|a| (a - mean).powi(2)).sum::<f64>() / (n * (n - 1)) as f64).sqrt();
assert!((mean - want).abs() < 4.0 * sd, "{mean} ± {sd} vs {want}");
}
}
#[test]
fn the_bhattacharyya_factor_of_bpsk_is_closed() {
let c = Constellation::psk(2).unwrap();
for snr_db in [-3.0, 0.0, 6.0] {
let s = db2lin(snr_db);
for (fading, b) in [(RateChannel::Awgn, (-s).exp()), (RateChannel::Rayleigh, 1.0 / (1.0 + s))] {
let want = 1.0 - b.ln_1p() / std::f64::consts::LN_2;
let got = bicm_cutoff_rate(&c, snr_db, fading, 40).unwrap();
assert!((got - want).abs() < 1e-6, "{snr_db} dB {fading:?}: {got} vs {want}");
assert!((got - cm_cutoff_rate(&c, snr_db, fading)).abs() < 1e-6);
}
}
}
#[test]
fn bicm_never_beats_cm_capacity_and_gray_beats_set_partitioning() {
for (kind, m) in [(ConstellationKind::Psk, 8), (ConstellationKind::Qam, 16)] {
let gray = Constellation::new(kind, m).unwrap();
let sp = Constellation::set_partitioning(kind, m).unwrap();
for snr_db in [0.0, 6.0, 12.0] {
let cm = constellation_mi(&gray, snr_db, None, RateChannel::Awgn, 16).unwrap();
let g = constellation_gmi(&gray, snr_db, None, RateChannel::Awgn, 16).unwrap();
let s = constellation_gmi(&sp, snr_db, None, RateChannel::Awgn, 16).unwrap();
assert!(cm >= g - 1e-12 && g > s, "{kind:?} {snr_db} dB: {cm} {g} {s}");
assert!((constellation_mi(&sp, snr_db, None, RateChannel::Awgn, 16).unwrap() - cm).abs() < 1e-9);
}
}
}
}
#[cfg(test)]
mod literature {
use super::*;
use crate::constellation::ConstellationKind;
use crate::theory::{constellation_gmi, constellation_mi};
use comnum_math::bisect;
fn pair(kind: ConstellationKind, m: usize) -> (Constellation, Constellation) {
(Constellation::new(kind, m).unwrap(), Constellation::set_partitioning(kind, m).unwrap())
}
#[test]
fn caire_1998_figs_4_5_capacity() {
let (p8, _) = pair(ConstellationKind::Psk, 8);
let (q16, sp16) = pair(ConstellationKind::Qam, 16);
for (got, want) in [
(constellation_mi(&p8, 10.0, None, RateChannel::Awgn, 12).unwrap(), 2.7),
(constellation_mi(&q16, 10.0, None, RateChannel::Awgn, 12).unwrap(), 3.15),
(constellation_gmi(&sp16, 10.0, None, RateChannel::Awgn, 12).unwrap(), 2.4),
(constellation_mi(&q16, 10.0, None, RateChannel::Rayleigh, 8).unwrap(), 2.6),
(constellation_gmi(&sp16, 15.0, None, RateChannel::Rayleigh, 8).unwrap(), 3.0),
] {
assert!((got - want).abs() < 0.1, "{got} vs {want}");
}
for (kind, m) in [(ConstellationKind::Psk, 4), (ConstellationKind::Psk, 8), (ConstellationKind::Qam, 16)] {
let (gray, sp) = pair(kind, m);
for snr_db in [-5.0, 0.0, 5.0, 10.0, 20.0] {
let gap = constellation_mi(&gray, snr_db, None, RateChannel::Awgn, 12).unwrap()
- constellation_gmi(&gray, snr_db, None, RateChannel::Awgn, 12).unwrap();
assert!((-1e-12..0.1).contains(&gap), "{kind:?} {m}, {snr_db} dB: {gap}");
}
if m > 4 {
let need = |c: &Constellation| {
bisect(|s| constellation_gmi(c, s, None, RateChannel::Awgn, 12).unwrap() < 2.0, -10.0, 30.0, 30)
};
let loss = need(&sp) - need(&gray);
assert!((2.0..6.0).contains(&loss), "{kind:?} {m}: {loss} dB");
}
}
}
}