use comnum_math::{db2lin, gaussian_nodes, ln_1p_exp, qfunc};
pub fn bpsk_noise_variance(ebn0_db: f64, rate: f64) -> f64 {
1.0 / (2.0 * rate * db2lin(ebn0_db))
}
fn info_density(l: f64) -> f64 {
1.0 - ln_1p_exp(-l) / std::f64::consts::LN_2
}
fn moments(noise_var: f64) -> (f64, f64) {
let mu = 2.0 / noise_var;
let mut m1 = 0.0;
let mut m2 = 0.0;
for (w, a) in gaussian_nodes(2.0 * mu, false, 64) {
let i = info_density(mu + w.re);
m1 += a * i;
m2 += a * i * i;
}
(m1, m2)
}
pub fn biawgn_capacity(noise_var: f64) -> f64 {
moments(noise_var).0
}
pub fn biawgn_dispersion(noise_var: f64) -> f64 {
let (m1, m2) = moments(noise_var);
(m2 - m1 * m1).max(0.0)
}
pub fn normal_approximation(n: usize, k: usize, ebn0_db: f64) -> f64 {
let s2 = bpsk_noise_variance(ebn0_db, k as f64 / n as f64);
NormalApproximation::biawgn(s2).block_error(n, k)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NormalApproximation {
pub capacity: f64,
pub dispersion: f64,
pub half_log: bool,
}
impl NormalApproximation {
pub fn bsc(delta: f64) -> NormalApproximation {
let h = comnum_math::entropy(&[delta, 1.0 - delta]);
let v = delta * (1.0 - delta) * ((1.0 - delta) / delta).log2().powi(2);
NormalApproximation { capacity: 1.0 - h, dispersion: v, half_log: true }
}
pub fn bec(delta: f64) -> NormalApproximation {
NormalApproximation { capacity: 1.0 - delta, dispersion: delta * (1.0 - delta), half_log: false }
}
pub fn awgn(snr_lin: f64) -> NormalApproximation {
let p = snr_lin;
let v = p * (p + 2.0) / (2.0 * (p + 1.0).powi(2)) * std::f64::consts::LOG2_E.powi(2);
NormalApproximation { capacity: 0.5 * (1.0 + p).log2(), dispersion: v, half_log: true }
}
pub fn biawgn(noise_var: f64) -> NormalApproximation {
let (m1, m2) = moments(noise_var);
NormalApproximation { capacity: m1, dispersion: (m2 - m1 * m1).max(0.0), half_log: true }
}
pub fn max_bits(&self, n: usize, eps: f64) -> f64 {
let nf = n as f64;
nf * self.capacity - (nf * self.dispersion).sqrt() * comnum_math::qfuncinv(eps) + self.third(nf)
}
pub fn block_error(&self, n: usize, k: usize) -> f64 {
let nf = n as f64;
qfunc((nf * self.capacity - k as f64 + self.third(nf)) / (nf * self.dispersion).sqrt())
}
pub fn blocklength(&self, rate: f64, eps: f64) -> Option<f64> {
if rate >= self.capacity {
return None;
}
let gap = |n: f64| {
n * (self.capacity - rate) - (n * self.dispersion).sqrt() * comnum_math::qfuncinv(eps) + self.third(n)
};
let mut hi = 2.0;
while gap(hi) < 0.0 {
hi *= 2.0;
if hi > 1e12 {
return None;
}
}
Some(comnum_math::bisect(|n| gap(n) < 0.0, 1.0, hi, 200))
}
fn third(&self, n: f64) -> f64 {
if self.half_log { 0.5 * n.log2() } else { 0.0 }
}
}
pub fn shannon_limit_ebn0_db(rate: f64) -> f64 {
comnum_math::bisect(|e| biawgn_capacity(bpsk_noise_variance(e, rate)) < rate, -2.0, 20.0, 100)
}
pub fn gaussian_limit_ebn0_db(eta: f64) -> f64 {
let lin = if eta.abs() < 1e-9 { std::f64::consts::LN_2 } else { (eta.exp2() - 1.0) / eta };
comnum_math::lin2db(lin)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_gaussian_limit_is_continuous_at_zero() {
assert!((gaussian_limit_ebn0_db(1e-6) - gaussian_limit_ebn0_db(0.0)).abs() < 1e-5);
assert!(gaussian_limit_ebn0_db(1.0).abs() < 1e-12);
for r in [0.1, 0.25, 0.5, 0.75, 0.9] {
assert!(shannon_limit_ebn0_db(r) > gaussian_limit_ebn0_db(2.0 * r));
}
}
fn former_moments(noise_var: f64) -> (f64, f64) {
let (t, w) = comnum_math::gauss_hermite(64);
let (mu, s) = (2.0 / noise_var, 2.0 / noise_var.sqrt());
let sp = std::f64::consts::PI.sqrt();
let (mut m1, mut m2) = (0.0, 0.0);
for (ti, wi) in t.iter().zip(&w) {
let i = info_density(mu + std::f64::consts::SQRT_2 * s * ti);
m1 += wi * i / sp;
m2 += wi * i * i / sp;
}
(m1, m2)
}
#[test]
fn the_shared_nodes_give_the_former_moments_to_the_rounding() {
for ebn0_db in [-2.0, 0.0, 2.0, 4.0, 8.0] {
let s2 = bpsk_noise_variance(ebn0_db, 0.5);
let ((a1, a2), (b1, b2)) = (moments(s2), former_moments(s2));
assert!((a1 - b1).abs() <= 1e-14 && (a2 - b2).abs() <= 1e-14, "{ebn0_db} dB: {a1} {b1}, {a2} {b2}");
}
}
#[test]
fn capacity_limits_and_monotonicity() {
assert!(biawgn_capacity(1e-3) > 0.999_999);
assert!(biawgn_capacity(1e3) < 1e-3);
assert!((biawgn_capacity(1.0) - 0.4859).abs() < 2e-3);
assert!((shannon_limit_ebn0_db(1.0 / 3.0) + 0.495).abs() < 0.02);
assert!(normal_approximation(1024, 512, 1.5) < normal_approximation(128, 64, 1.5));
}
#[test]
fn the_approximation_inverts_itself() {
let c = NormalApproximation::bsc(0.11);
let k = c.max_bits(1000, 1e-3);
assert!((c.block_error(1000, k.round() as usize) / 1e-3).ln().abs() < 0.2);
assert!(NormalApproximation::bec(0.5).blocklength(0.5, 1e-3).is_none());
}
}
#[cfg(test)]
mod literature {
use super::*;
#[test]
fn polyanskiy_poor_verdu_2010_table_i() {
let cases = [
(NormalApproximation::bec(0.5), 1e-3, 955.0),
(NormalApproximation::bsc(0.11), 1e-3, 3150.0),
(NormalApproximation::awgn(1.0), 1e-3, 2750.0),
(NormalApproximation::awgn(100.0), 1e-6, 190.0),
];
for (c, eps, want) in cases {
let n = c.blocklength(0.9 * c.capacity, eps).unwrap();
assert!((n / want - 1.0).abs() < 0.01, "{c:?}: n = {n} vs {want}");
}
}
}