use comnum_core::{Error, Result};
use comnum_math::{bisect, expint_e1};
use std::f64::consts::{LN_2, PI};
pub fn mqam_ber_bound(snr: f64, m: f64) -> f64 {
0.2 * (-1.5 * snr / (m - 1.0)).exp()
}
pub fn mqam_power_gap(ber: f64) -> Result<f64> {
if !(ber > 0.0 && ber < 0.2) {
return Err(Error::Config(format!("the target bit error rate must be in (0, 0.2), got {ber}")));
}
Ok(-1.5 / (5.0 * ber).ln())
}
fn check_snr(mean_snr: f64) -> Result<()> {
if mean_snr > 0.0 && mean_snr.is_finite() {
Ok(())
} else {
Err(Error::Config(format!("the mean SNR must be positive and finite, got {mean_snr}")))
}
}
pub fn rayleigh_water_filling_cutoff(mean_snr: f64) -> Result<f64> {
check_snr(mean_snr)?;
let ln_x = bisect(|l| (-l.exp()).exp() / l.exp() - expint_e1(l.exp()) > mean_snr, -50.0, 5.0, 200);
Ok(ln_x.exp() * mean_snr)
}
pub fn rayleigh_capacity_water_filling(mean_snr: f64) -> Result<f64> {
let g0 = rayleigh_water_filling_cutoff(mean_snr)?;
Ok(expint_e1(g0 / mean_snr) / LN_2)
}
pub fn rayleigh_adaptive_mqam_efficiency(mean_snr: f64, ber: f64) -> Result<f64> {
rayleigh_capacity_water_filling(mqam_power_gap(ber)? * mean_snr)
}
#[derive(Clone, Debug, PartialEq)]
pub struct DiscreteRate {
pub gamma_k: f64,
pub efficiency: f64,
pub edges: Vec<f64>,
}
pub fn rayleigh_discrete_mqam(mean_snr: f64, ber: f64, sizes: &[usize]) -> Result<DiscreteRate> {
check_snr(mean_snr)?;
let k = mqam_power_gap(ber)?;
if sizes.is_empty() || sizes[0] < 2 || sizes.windows(2).any(|w| w[0] >= w[1]) {
return Err(Error::Config(format!("the constellation sizes must be increasing and at least 2, got {sizes:?}")));
}
let m: Vec<f64> = sizes.iter().map(|&s| s as f64).collect();
let e1 = |a: f64| if a.is_infinite() { 0.0 } else { expint_e1(a / mean_snr) };
let power = |g: f64| -> f64 {
(0..m.len())
.map(|j| {
let next = m.get(j + 1).map_or(f64::INFINITY, |&n| g * n);
(m[j] - 1.0) / (k * mean_snr) * (e1(g * m[j]) - e1(next))
})
.sum()
};
let ln_g = bisect(|l| power(l.exp()) > 1.0, -60.0, 20.0, 200);
let g = ln_g.exp();
let edges: Vec<f64> = m.iter().map(|&mj| g * mj).collect();
let surv = |a: f64| (-a / mean_snr).exp();
let efficiency = (0..m.len())
.map(|j| {
let next = edges.get(j + 1).map_or(0.0, |&a| surv(a));
m[j].log2() * (surv(edges[j]) - next)
})
.sum();
Ok(DiscreteRate { gamma_k: g, efficiency, edges })
}
pub fn rayleigh_region_durations(policy: &DiscreteRate, mean_snr: f64, doppler_hz: f64) -> Result<Vec<f64>> {
check_snr(mean_snr)?;
if !(doppler_hz > 0.0 && doppler_hz.is_finite()) {
return Err(Error::Config(format!("the Doppler shift must be positive and finite, got {doppler_hz}")));
}
let a: Vec<f64> = std::iter::once(0.0).chain(policy.edges.iter().copied()).collect();
let rate = |a: f64| (2.0 * PI * a / mean_snr).sqrt() * doppler_hz * (-a / mean_snr).exp();
let surv = |a: f64| (-a / mean_snr).exp();
Ok((0..a.len())
.map(|j| {
let (lo, hi) = (a[j], a.get(j + 1).copied());
let prob = surv(lo) - hi.map_or(0.0, surv);
let crossings = rate(lo) + hi.map_or(0.0, rate);
prob / crossings
})
.collect())
}
pub fn estimation_error_ber(ber0: f64, epsilon: f64) -> f64 {
0.2 * (5.0 * ber0).powf(1.0 / epsilon)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_cutoff_solves_eq_6_and_mqam_pays_the_gap() {
for snr_db in [0.0, 10.0, 20.0, 30.0] {
let s: f64 = 10f64.powf(snr_db / 10.0);
let g0 = rayleigh_water_filling_cutoff(s).unwrap();
let x = g0 / s;
assert!(((-x).exp() / g0 - expint_e1(x) / s - 1.0).abs() < 1e-9, "{snr_db} dB");
assert!(g0 < 1.0);
let c = rayleigh_capacity_water_filling(s).unwrap();
let r = rayleigh_adaptive_mqam_efficiency(s, 1e-3).unwrap();
assert!(r < c);
}
assert!((mqam_ber_bound(0.0, 4.0) - 0.2).abs() < 1e-15);
assert!((10.0 * mqam_power_gap(1e-3).unwrap().log10() + 5.48).abs() < 0.01);
assert!(mqam_power_gap(0.3).is_err() && rayleigh_capacity_water_filling(-1.0).is_err());
}
#[test]
fn discrete_rates_approach_the_continuous_one_and_saturate() {
let s = 100.0;
let sizes: Vec<usize> = (1..=14).map(|b| 1usize << b).collect();
let d = rayleigh_discrete_mqam(s, 1e-3, &sizes).unwrap();
let c = rayleigh_adaptive_mqam_efficiency(s, 1e-3).unwrap();
assert!(d.efficiency < c && c - d.efficiency < 0.6, "{} {c}", d.efficiency);
let one = rayleigh_discrete_mqam(s, 1e-3, &[2]).unwrap();
assert!(one.efficiency < 1.0 && one.efficiency > 0.9);
assert!(rayleigh_discrete_mqam(s, 1e-3, &[4, 2]).is_err());
let t = rayleigh_region_durations(&d, s, 100.0).unwrap();
assert_eq!(t.len(), sizes.len() + 1);
assert!(t.iter().all(|&x| x > 0.0 && x.is_finite()));
}
}