use comnum_math::{C64, erf, expint_e1};
pub fn gain_limited(r: &[C64], pilot: &[C64], eps: f64) -> Vec<C64> {
r.iter()
.zip(pilot)
.map(|(r, p)| {
let a = p.norm();
if a >= eps { r / p } else { r / (p * (eps / a)) }
})
.collect()
}
pub fn sdr_gain_limited(sir_lin: f64, eps: f64, rho: C64) -> f64 {
let e2 = eps * eps;
let gamma = 1.0 / sir_lin;
let d = (1.0 + gamma) * -(-e2).exp_m1() / e2 + 1.0 - rho.re * std::f64::consts::PI.sqrt() * erf(eps) / eps
+ (1.0 - rho.norm_sqr() + gamma) * expint_e1(e2);
1.0 / d
}
pub fn sdr_optimum(sir_lin: f64) -> f64 {
if (sir_lin - 1.0).abs() < 1e-6 {
return 1.0 / (0.5 - (sir_lin - 1.0) / 6.0);
}
let s = sir_lin - 1.0;
s / (sir_lin / s * sir_lin.ln() - 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use comnum_core::rng::Rng;
fn distortion(sir: f64, rho: C64, correct: impl Fn(C64, C64, C64, f64) -> C64) -> (f64, f64) {
let mut r = Rng::new(17);
let n: usize = 400_000;
let g = 1.0 / sir;
let c = comnum_mapping::constellation::Constellation::qam(4).unwrap().normalized();
let qpsk = |r: &mut Rng| c.points()[r.below(4)];
let v: Vec<f64> = (0..n)
.map(|_| {
let (zd, zi, zx) = (r.complex_gaussian(), r.complex_gaussian(), r.complex_gaussian());
let zp = rho.conj() * zd + (1.0 - rho.norm_sqr()).sqrt() * zx;
let (d, i) = (qpsk(&mut r), qpsk(&mut r));
let rx = zd * d + g.sqrt() * zi * i;
(correct(rx, zp, zd, zi.norm_sqr() * g) - d).norm_sqr()
})
.collect();
let mean = v.iter().sum::<f64>() / n as f64;
let sd = (v.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n * (n - 1)) as f64).sqrt();
(mean, sd)
}
#[test]
fn eq_25_is_the_distortion_of_gain_limited_correction() {
for (sir, eps, rho) in [
(100.0, 0.3, C64::new(1.0, 0.0)),
(10.0, 0.6, C64::new(1.0, 0.0)),
(f64::INFINITY, 0.2, C64::from_polar(0.95, -0.2)),
(30.0, 0.4, C64::from_polar(0.8, 0.5)),
] {
let (d, sd) = distortion(sir, rho, |rx, zp, _, _| gain_limited(&[rx], &[zp], eps)[0]);
let want = 1.0 / sdr_gain_limited(sir, eps, rho);
assert!((d - want).abs() < 4.0 * sd, "SIR {sir}, ε {eps}: {d} ± {sd} vs {want}");
}
}
#[test]
fn eq_21_is_the_distortion_of_the_optimum_correction() {
for sir in [0.5, 1.0, 3.0, 100.0] {
let (d, sd) =
distortion(sir, C64::new(1.0, 0.0), |rx, _, zd, gai2| rx / (zd * (1.0 + gai2 / zd.norm_sqr())));
let want = 1.0 / sdr_optimum(sir);
assert!((d - want).abs() < 4.0 * sd, "SIR {sir}: {d} ± {sd} vs {want}");
}
assert!((sdr_optimum(1.0 + 1e-9) - sdr_optimum(1.0 + 2e-6)).abs() < 1e-5);
}
#[test]
fn gain_limited_correction_follows_the_fades_down_to_the_limit() {
let r = [C64::new(0.5, 0.0), C64::new(0.0, 0.01)];
let p = [C64::new(0.0, 0.5), C64::new(0.0, 0.01)];
let d = gain_limited(&r, &p, 0.1);
assert!((d[0] - C64::new(0.0, -1.0)).norm() < 1e-15);
assert!((d[1] - C64::new(0.1, 0.0)).norm() < 1e-15);
}
}
#[cfg(test)]
mod literature {
use super::*;
fn db(x: f64) -> f64 {
comnum_math::lin2db(x)
}
#[test]
fn cimini_1985_figs_3_4() {
let one = C64::new(1.0, 0.0);
let curve =
|sir_db: f64, e_db: f64| db(sdr_gain_limited(comnum_math::db2lin(sir_db), comnum_math::db2amp(e_db), one));
for (sir, peak, at, low) in
[(25.0, 17.9, -16.7, 16.2), (20.0, 13.8, -11.6, 11.2), (15.0, 10.1, -7.2, 6.2), (10.0, 6.9, -3.3, 1.2)]
{
let gains: Vec<f64> = (0..=400).map(|k| -30.0 + 0.1 * k as f64).collect();
let (k, v) = comnum_math::argmax(gains.iter().map(|&e| curve(sir, e))).unwrap();
let e = gains[k];
assert!((v - peak).abs() < 0.3 && (e - at).abs() < 1.0, "SIR {sir}: {v} dB at {e} dB");
assert!((curve(sir, -30.0) - low).abs() < 0.2, "SIR {sir}");
let opt = db(sdr_optimum(comnum_math::db2lin(sir)));
assert!(opt > v && opt - v < 1.0, "SIR {sir}: {opt}");
}
for (sir, read) in [(0.0, 3.0), (20.0, 14.1), (40.0, 31.0)] {
assert!((db(sdr_optimum(comnum_math::db2lin(sir))) - read).abs() < 0.3, "SIR {sir}");
}
}
}