use crate::constellation::{Constellation, ConstellationKind};
use comnum_core::{Error, Result, ensure};
use comnum_math::bessel_j0;
use std::f64::consts::{FRAC_1_SQRT_2, PI};
pub fn ber_imperfect_csi(c: &Constellation, mean_ebn0_lin: f64, rho1: f64, rho2: f64, sigma_ratio: f64) -> Result<f64> {
match (c.kind(), c.order()) {
(ConstellationKind::Psk, 2) => ber_bpsk_imperfect_csi(mean_ebn0_lin, rho1, rho2),
(ConstellationKind::Qam | ConstellationKind::Psk, 4) => ber_qpsk_imperfect_csi(mean_ebn0_lin, rho1, rho2),
(ConstellationKind::Qam, 16) => ber_16qam_imperfect_csi(mean_ebn0_lin, rho1, rho2, sigma_ratio),
(kind, m) => Err(Error::Config(format!(
"the BER with an imperfect estimate is for BPSK, QPSK and 16-QAM, got {m}-{}",
kind.name()
))),
}
}
fn check(mean_ebn0_lin: f64, rho1: f64, rho2: f64) -> Result<()> {
ensure_snr(mean_ebn0_lin)?;
ensure(rho1.is_finite() && rho2.is_finite() && rho1 * rho1 + rho2 * rho2 <= 1.0, || {
format!("the correlations of the estimate need ρ1² + ρ2² ≤ 1, got ({rho1}, {rho2})")
})
}
fn ensure_snr(mean_ebn0_lin: f64) -> Result<()> {
ensure(mean_ebn0_lin > 0.0, || format!("the mean SNR per bit must be positive, got {mean_ebn0_lin}"))
}
pub fn ber_bpsk_imperfect_csi(mean_ebn0_lin: f64, rho1: f64, rho2: f64) -> Result<f64> {
check(mean_ebn0_lin, rho1, rho2)?;
Ok(0.5 * (1.0 - rho1 / (1.0 + 1.0 / mean_ebn0_lin - rho2 * rho2).sqrt()))
}
pub fn ber_qpsk_imperfect_csi(mean_ebn0_lin: f64, rho1: f64, rho2: f64) -> Result<f64> {
check(mean_ebn0_lin, rho1, rho2)?;
let d = 1.0 + 1.0 / (2.0 * mean_ebn0_lin);
let (s, t) = (rho1 + rho2, rho1 - rho2);
Ok(0.5
* (1.0
- 0.5 * s * FRAC_1_SQRT_2 / (d - t * t / 2.0).sqrt()
- 0.5 * t * FRAC_1_SQRT_2 / (d - s * s / 2.0).sqrt()))
}
#[derive(Clone, Copy)]
struct TableRow {
sign: f64,
a: f64,
b: f64,
cos: f64,
sin: f64,
}
type TableGroup = ([(f64, f64, f64); 3], f64, f64);
fn table_i() -> [TableRow; 24] {
let (r2, r10) = (2f64.sqrt(), 10f64.sqrt());
let groups: [TableGroup; 8] = [
([(1.0, 2.0, -r2), (1.0, 0.0, r2), (1.0, 2.0, r2)], 1.0 / r2, 1.0 / r2),
([(1.0, 2.0, -r2), (1.0, 0.0, r2), (1.0, 2.0, r2)], 1.0 / r2, -1.0 / r2),
([(1.0, 2.0, -r10), (1.0, 0.0, r10), (1.0, 2.0, r10)], 1.0 / r10, 3.0 / r10),
([(1.0, -2.0, r10), (1.0, 0.0, r10), (-1.0, 2.0, r10)], 3.0 / r10, -1.0 / r10),
([(1.0, -2.0, r10), (1.0, 0.0, r10), (-1.0, 2.0, r10)], 3.0 / r10, 1.0 / r10),
([(1.0, 2.0, -r10), (1.0, 0.0, r10), (1.0, 2.0, r10)], 1.0 / r10, -3.0 / r10),
([(1.0, -2.0, 3.0 * r2), (1.0, 0.0, 3.0 * r2), (-1.0, 2.0, 3.0 * r2)], 1.0 / r2, 1.0 / r2),
([(1.0, -2.0, 3.0 * r2), (1.0, 0.0, 3.0 * r2), (-1.0, 2.0, 3.0 * r2)], 1.0 / r2, -1.0 / r2),
];
let mut t = [TableRow { sign: 0.0, a: 0.0, b: 0.0, cos: 0.0, sin: 0.0 }; 24];
for (g, (rows, cos, sin)) in groups.iter().enumerate() {
for (i, &(sign, a, b)) in rows.iter().enumerate() {
t[3 * g + i] = TableRow { sign, a, b, cos: *cos, sin: *sin };
}
}
t
}
pub fn ber_16qam_imperfect_csi(mean_ebn0_lin: f64, rho1: f64, rho2: f64, sigma_ratio: f64) -> Result<f64> {
check(mean_ebn0_lin, rho1, rho2)?;
ensure(sigma_ratio > 0.0 && sigma_ratio.is_finite(), || {
format!("the standard deviation ratio must be positive, got {sigma_ratio}")
})?;
let rho_sq = rho1 * rho1 + rho2 * rho2;
Ok(table_i()
.iter()
.map(|r| {
let u = r.b * (rho1 * r.cos + rho2 * r.sin) + r.a * sigma_ratio;
r.sign * (1.0 - u / (r.b * r.b * (1.0 - rho_sq) + u * u + 5.0 / (2.0 * mean_ebn0_lin)).sqrt())
})
.sum::<f64>()
/ 32.0)
}
pub fn ber_16qam_rayleigh(mean_ebn0_lin: f64) -> Result<f64> {
ensure_snr(mean_ebn0_lin)?;
let t = |c: f64| 1.0 / (1.0 + c / mean_ebn0_lin).sqrt();
Ok(0.5 - 3.0 / 8.0 * t(5.0 / 2.0) - 0.25 * t(5.0 / 18.0) + 0.125 * t(1.0 / 10.0))
}
pub fn ber_dbpsk_jakes(mean_ebn0_lin: f64, fd_ts: f64) -> Result<f64> {
ensure_snr(mean_ebn0_lin)?;
ensure(fd_ts.is_finite(), || format!("the normalised Doppler must be finite, got {fd_ts}"))?;
Ok(0.5 * (1.0 - bessel_j0(2.0 * PI * fd_ts) / (1.0 + 1.0 / mean_ebn0_lin)))
}
pub fn ber_dqpsk_jakes(mean_ebn0_lin: f64, fd_ts: f64) -> Result<f64> {
ensure_snr(mean_ebn0_lin)?;
ensure(fd_ts.is_finite(), || format!("the normalised Doppler must be finite, got {fd_ts}"))?;
let j = bessel_j0(2.0 * PI * fd_ts);
Ok(0.5 * (1.0 - j / ((2.0 + 1.0 / mean_ebn0_lin) * (1.0 + 1.0 / (2.0 * mean_ebn0_lin)) - j * j).sqrt()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::theory::ber_rayleigh_mrc;
#[test]
fn the_constellation_selects_the_closed_form() {
let (g, r1, r2) = (30.0, 0.98, 0.05);
let at = |name: &str| ber_imperfect_csi(&Constellation::from_name(name).unwrap(), g, r1, r2, 0.9);
assert_eq!(at("bpsk").unwrap(), ber_bpsk_imperfect_csi(g, r1, r2).unwrap());
assert_eq!(at("QPSK").unwrap(), ber_qpsk_imperfect_csi(g, r1, r2).unwrap());
assert_eq!(at("4psk").unwrap(), ber_qpsk_imperfect_csi(g, r1, r2).unwrap());
assert_eq!(at("16-QAM").unwrap(), ber_16qam_imperfect_csi(g, r1, r2, 0.9).unwrap());
assert!(at("8psk").is_err() && at("64qam").is_err());
}
#[test]
fn the_perfect_estimate_gives_the_known_results() {
for g in [0.5, 3.0, 10.0, 100.0, 1e4] {
let r = ber_rayleigh_mrc(g, 1).unwrap();
assert!((ber_bpsk_imperfect_csi(g, 1.0, 0.0).unwrap() - r).abs() < 1e-15);
assert!((ber_qpsk_imperfect_csi(g, 1.0, 0.0).unwrap() - r).abs() < 1e-15);
assert!(
(ber_16qam_imperfect_csi(g, 1.0, 0.0, 1.0).unwrap() - ber_16qam_rayleigh(g).unwrap()).abs() < 1e-14,
"{g}"
);
assert!((ber_dbpsk_jakes(g, 0.0).unwrap() - 0.5 / (1.0 + g)).abs() < 1e-15);
let dq = 0.5 * (1.0 - 1.0 / (1.0 + 2.0 / g + 1.0 / (2.0 * g * g)).sqrt());
assert!((ber_dqpsk_jakes(g, 0.0).unwrap() - dq).abs() < 1e-15);
}
let g = 1e6;
assert!((ber_bpsk_imperfect_csi(g, 1.0, 0.0).unwrap() * 4.0 * g - 1.0).abs() < 1e-5);
assert!((ber_16qam_rayleigh(g).unwrap() * g - (15.0 / 32.0 + 5.0 / 144.0 - 1.0 / 160.0)).abs() < 1e-5);
}
}