use super::Css;
use comnum_core::Result;
use comnum_math::{C64, cis, db2lin, fft, fresnel, qfunc};
use std::f64::consts::{PI, TAU};
fn chips(sf: u32) -> Result<usize> {
Css::lora(sf)?;
Ok(1 << sf)
}
pub fn ser(sf: u32, snr_db: f64) -> Result<f64> {
let m = chips(sf)?;
comnum_mapping::theory::ser_orthogonal_noncoherent(m, snr_db + comnum_math::lin2db(m as f64))
}
pub fn ber(sf: u32, snr_db: f64) -> Result<f64> {
let m = chips(sf)?;
comnum_mapping::theory::ber_orthogonal_noncoherent(m, snr_db + comnum_math::lin2db(m as f64))
}
fn harmonic(n: usize) -> f64 {
(1..=n).map(|k| 1.0 / k as f64).sum()
}
pub fn ser_rice(sf: u32, snr_db: f64) -> Result<f64> {
let m = chips(sf)?;
let h = harmonic(m - 1);
let d = (h * h - PI * PI / 12.0).sqrt();
let gamma = m as f64 * db2lin(snr_db);
Ok(qfunc((gamma.sqrt() - d.sqrt()) / (h - d + 0.5).sqrt()))
}
pub fn ser_gumbel(sf: u32, snr_db: f64) -> Result<f64> {
const EULER: f64 = 0.577_215_664_901_532_9;
let gamma = chips(sf)? as f64 * db2lin(snr_db);
Ok(qfunc((2.0 * gamma).sqrt() - (2.0 * (std::f64::consts::LN_2 * f64::from(sf) + EULER)).sqrt()))
}
pub fn cross_correlation(sf: u32, l: usize, m_sym: usize) -> Result<C64> {
let m = chips(sf)?;
comnum_core::ensure(l < m && m_sym < m, || format!("the symbols {l} and {m_sym} must lie below M = {m}"))?;
Ok(correlation(m as f64, l, m_sym))
}
fn correlation(m: f64, l: usize, m_sym: usize) -> C64 {
if l == m_sym {
return C64::new(1.0, 0.0);
}
let (l, q) = (l as f64, m_sym as f64);
let d = (q - l).abs();
let num = cis(TAU * l * (q - l) / m) - cis(TAU * q * (q - l) / m);
num * m / (C64::new(0.0, TAU) * (m - d) * d)
}
pub fn max_cross_correlation(sf: u32) -> Result<f64> {
let m = chips(sf)?;
let mut best = f64::NEG_INFINITY;
for l in 0..m {
for q in 0..m {
if l != q {
best = best.max(correlation(m as f64, l, q).re);
}
}
}
Ok(best)
}
fn fresnel_k(x: f64) -> C64 {
let (c, s) = fresnel(x);
C64::new(c, s)
}
struct Shared {
m: f64,
c: f64,
d: C64,
w1: C64,
root: f64,
}
fn chirp_phase(m: usize, x: f64) -> C64 {
cis(-PI * x * x / m as f64)
}
impl Shared {
fn new(m: usize, f: f64) -> Shared {
let m = m as f64;
let s = (2.0 / m).sqrt();
let w = cis(-TAU * f * m);
let d = fresnel_k(s * (m / 2.0 - f * m)) - w * fresnel_k(s * (-m / 2.0 - f * m));
Shared { m, c: m / 2.0 + f * m, d, w1: w - 1.0, root: (m / 2.0).sqrt() }
}
fn spectrum(&self, phase: C64, k: C64) -> C64 {
phase * (self.d + self.w1 * k) * self.root
}
fn density(&self, terms: impl Iterator<Item = C64>) -> f64 {
let (energy, sum) = terms.fold((0.0, C64::new(0.0, 0.0)), |(e, s), x| (e + x.norm_sqr(), s + x));
(energy - sum.norm_sqr() / self.m) / (self.m * self.m)
}
}
pub fn continuous_spectrum(sf: u32, f: &[f64]) -> Result<Vec<f64>> {
let m = chips(sf)?;
let s = (2.0 / m as f64).sqrt();
Ok(f.iter()
.map(|&fk| {
let shared = Shared::new(m, fk);
shared.density((0..m).map(|l| {
let x = l as f64 - shared.c;
shared.spectrum(chirp_phase(m, x), fresnel_k(s * x))
}))
})
.collect())
}
fn continuous_spectrum_grid(m: usize, n: usize) -> Vec<f64> {
let s = (2.0 / m as f64).sqrt();
let mf = m as f64;
let table: Vec<(C64, C64)> = (0..8 * (m - 1) + n)
.map(|j| {
let x = (j as f64 - (n - 1) as f64 - 4.0 * mf - 0.5) / 8.0;
(chirp_phase(m, x), fresnel_k(s * x))
})
.collect();
(0..n)
.map(|i| {
let shared = Shared::new(m, (i as f64 + 0.5) / (8.0 * mf));
shared.density((0..m).map(|l| {
let (phase, k) = table[8 * l + n - 1 - i];
shared.spectrum(phase, k)
}))
})
.collect()
}
pub fn spectral_lines(sf: u32, n_max: usize) -> Result<Vec<(f64, f64)>> {
let m = chips(sf)?;
let mf = m as f64;
let s = (2.0 / mf).sqrt();
let n_max = n_max as i64;
Ok((-n_max..=n_max)
.map(|n| {
let n = n as f64;
let d = fresnel_k(s * (mf / 2.0 - n)) + fresnel_k(s * (mf / 2.0 + n));
(n / mf, d.norm_sqr() / (2.0 * mf * mf))
})
.collect())
}
pub fn occupied_bandwidth(sf: u32, fraction: f64, span: f64) -> Result<f64> {
let m = chips(sf)?;
comnum_core::ensure(fraction > 0.0 && fraction < 1.0 && span > 0.0, || {
format!("a fraction in (0, 1) and a positive span, got {fraction} and {span}")
})?;
let step = 1.0 / (8.0 * m as f64);
let n = (span / step).ceil() as usize;
let g = continuous_spectrum_grid(m, n);
let mut lines = spectral_lines(sf, (span * m as f64).ceil() as usize)?;
let mut inside = lines.iter().filter(|(fl, _)| *fl == 0.0).map(|l| l.1).sum::<f64>();
lines.sort_by(|a, b| a.0.abs().total_cmp(&b.0.abs()));
let (mut lo, mut hi) = (0, 0);
let mut prev = (0.0, inside);
for (i, gi) in g.iter().enumerate() {
let z = (i + 1) as f64 * step;
inside += 2.0 * gi * step;
while lo < lines.len() && lines[lo].0.abs() <= z - step {
lo += 1;
}
hi = hi.max(lo);
while hi < lines.len() && lines[hi].0.abs() <= z {
hi += 1;
}
inside += lines[lo..hi].iter().map(|l| l.1).sum::<f64>();
if inside >= fraction {
let (z0, p0) = prev;
return Ok(2.0 * (z0 + (z - z0) * (fraction - p0) / (inside - p0)));
}
prev = (z, inside);
}
Ok(2.0 * span)
}
pub fn interference_pattern(sf: u32, s1: usize, s2: usize, tau: f64) -> Result<Vec<C64>> {
let css = Css::lora(sf)?;
let x = css.interferer(s1, s2, tau)?;
let reference = css.symbol(0)?;
Ok(fft::fft(&x.iter().zip(&reference).map(|(a, b)| a * b.conj()).collect::<Vec<_>>()))
}
pub fn interference_magnitudes(sf: u32, s1: usize, s2: usize, tau: f64) -> Result<Vec<f64>> {
let m = chips(sf)?;
comnum_core::ensure(s1 < m && s2 < m, || format!("the symbols must lie below M = {m}"))?;
comnum_core::ensure((0.0..m as f64).contains(&tau), || format!("the offset must lie in [0, {m}), got {tau}"))?;
let mf = m as f64;
let c = tau.ceil();
let (l_int, lambda) = (tau.floor(), tau - tau.floor());
let ratio = |x: f64, len: f64| sine_ratio(x, len, mf);
let (s1f, s2f) = (s1 as f64, s2 as f64);
Ok((0..m)
.map(|k| {
let k = k as f64;
let a1 = ratio(s1f - k - tau, c);
let a2 = ratio(s2f - k - tau, mf - c);
let t1 = PI / mf
* (-tau * tau + (lambda - l_int) * mf + s1f * (2.0 * tau - c + 1.0) + k * (c - 1.0) + tau * (c - 1.0));
let t2 = PI / mf * (-tau * tau + s2f * (2.0 * tau - c + 1.0 - mf) + k * (c - 1.0 + mf) + tau * (c - 1.0));
(a1 * a1 + a2 * a2 + 2.0 * a1 * a2 * (t1 - t2).cos()).max(0.0).sqrt()
})
.collect())
}
fn sine_ratio(x: f64, len: f64, m: f64) -> f64 {
if len == 0.0 { 0.0 } else { len * comnum_math::diric(TAU * x / m, len as usize) }
}
fn strongest_bin(m: usize, si: usize, tau: f64) -> f64 {
let mf = m as f64;
let c = tau.ceil();
let k = -tau.round();
(sine_ratio(si as f64 - k - tau, c, mf) + sine_ratio(-k - tau, mf - c, mf)).abs()
}
fn interference_error(m: usize, si: usize, tau: f64, rho: f64, pi_lin: f64) -> f64 {
let mf = m as f64;
qfunc((mf - pi_lin.sqrt() * strongest_bin(m, si, tau)) / (mf / rho).sqrt())
}
const PAPER_OFFSET_STEP: f64 = 0.2;
const MIN_OFFSET_STEP: f64 = 1e-3;
fn offsets(half: f64, eps: f64) -> (f64, Vec<f64>) {
let eps = if eps.is_finite() { eps.max(MIN_OFFSET_STEP) } else { PAPER_OFFSET_STEP };
let n = (half / eps).ceil().max(0.0) as usize;
(eps, (0..n).map(|i| i as f64 * eps).filter(|t| *t < half).collect())
}
pub fn ser_interference(sf: u32, snr_db: f64, pi_db: f64, eps: f64) -> Result<f64> {
let m = chips(sf)?;
let (r, pi) = (db2lin(snr_db), comnum_math::db2lin(pi_db));
let half = (m as f64 - 1.0) / 2.0;
let (eps, taus) = offsets(half, eps);
let mut acc = 0.0;
for si in 0..m {
for &tau in &taus {
acc += interference_error(m, si, tau, r, pi);
}
}
let p_int = acc * eps / (m as f64 * (m as f64 / 2.0));
let p_noise = ser(sf, snr_db)?;
Ok(p_noise + (1.0 - p_noise) * p_int)
}
pub fn fer_interference(sf: u32, snr_db: f64, pi_db: f64, frame: usize, eps: f64) -> Result<f64> {
let m = chips(sf)?;
let (r, pi) = (db2lin(snr_db), comnum_math::db2lin(pi_db));
let half = (m as f64 - 1.0) / 2.0;
let (eps, taus) = offsets(half, eps);
let p_tau: Vec<f64> = taus
.iter()
.map(|&tau| (0..m).map(|si| interference_error(m, si, tau, r, pi)).sum::<f64>() / m as f64)
.collect();
let p_noise = ser(sf, snr_db)?;
let mut total = 0.0;
for fi in 1..=frame {
let ok: f64 = p_tau.iter().map(|p| (1.0 - p).powi(fi as i32) * (1.0 - p_noise).powi((frame - fi) as i32)).sum();
total += 2.0 / m as f64 * eps * (taus.len() as f64 - ok);
}
Ok((total / frame as f64).clamp(0.0, 1.0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::css::Waveform;
use comnum_core::rng::Rng;
fn former_waveform_spectrum(m: usize, l: usize, f: f64) -> C64 {
let segment = |a: f64, b: f64, t1: f64, t2: f64| {
let r = b.sqrt();
let shift = a / (2.0 * b);
cis(-TAU * a * a / (4.0 * b)) / (2.0 * r)
* (fresnel_k(2.0 * r * (t2 + shift)) - fresnel_k(2.0 * r * (t1 + shift)))
};
let (mf, lf) = (m as f64, l as f64);
let b = 1.0 / (2.0 * mf);
let fold = mf - lf;
segment(lf / mf - 0.5 - f, b, 0.0, fold) + segment(lf / mf - 1.5 - f, b, fold, mf)
}
fn former_continuous_spectrum(m: usize, f: f64) -> f64 {
let mf = m as f64;
let x: Vec<C64> = (0..m).map(|l| former_waveform_spectrum(m, l, f)).collect();
let sum: C64 = x.iter().sum();
(x.iter().map(C64::norm_sqr).sum::<f64>() - sum.norm_sqr() / mf) / (mf * mf)
}
fn former_occupied_bandwidth(sf: u32, fraction: f64, span: f64) -> f64 {
let m = 1usize << sf;
let mf = m as f64;
let step = 1.0 / (8.0 * mf);
let n = (span / step).ceil() as usize;
let g: Vec<f64> = (0..n).map(|i| former_continuous_spectrum(m, (i as f64 + 0.5) * step)).collect();
let n_max = (span * mf).ceil() as i64;
let lines: Vec<(f64, f64)> = (-n_max..=n_max)
.map(|k| {
let f = k as f64 / mf;
let sum: C64 = (0..m).map(|l| former_waveform_spectrum(m, l, f)).sum();
(f, sum.norm_sqr() / (mf * mf * mf * mf))
})
.collect();
let mut inside = lines.iter().filter(|(fl, _)| *fl == 0.0).map(|l| l.1).sum::<f64>();
let mut prev = (0.0, inside);
for (i, gi) in g.iter().enumerate() {
let z = (i + 1) as f64 * step;
inside += 2.0 * gi * step;
inside += lines.iter().filter(|(fl, _)| fl.abs() > z - step && fl.abs() <= z).map(|l| l.1).sum::<f64>();
if inside >= fraction {
let (z0, p0) = prev;
return 2.0 * (z0 + (z - z0) * (fraction - p0) / (inside - p0));
}
prev = (z, inside);
}
2.0 * span
}
#[test]
fn the_spectrum_is_the_former_evaluation_of_eq_25() {
let mut rng = Rng::new(9);
for sf in [1u32, 3, 5, 8] {
let m = 1usize << sf;
let f: Vec<f64> = (0..40).map(|_| 3.0 * rng.uniform() - 1.5).collect();
let peak = former_continuous_spectrum(m, 0.0).max(1e-3);
for (fk, g) in f.iter().zip(continuous_spectrum(sf, &f).unwrap()) {
let want = former_continuous_spectrum(m, *fk);
assert!((g - want).abs() < 1e-12 * peak, "SF {sf}, f {fk}: {g} vs {want}");
}
let n = 8 * m + 5;
let mid: Vec<f64> = (0..n).map(|i| (i as f64 + 0.5) / (8.0 * m as f64)).collect();
let direct = continuous_spectrum(sf, &mid).unwrap();
for (a, b) in continuous_spectrum_grid(m, n).iter().zip(&direct) {
assert!((a - b).abs() < 1e-12 * peak, "SF {sf}: {a} vs {b}");
}
for (fl, p) in spectral_lines(sf, 3 * m).unwrap() {
let sum: C64 = (0..m).map(|l| former_waveform_spectrum(m, l, fl)).sum();
let want = sum.norm_sqr() / (m as f64).powi(4);
assert!((p - want).abs() < 1e-12 / m as f64, "SF {sf}, line {fl}: {p} vs {want}");
}
}
for (sf, span) in [(3u32, 3.0), (5, 3.0), (7, 0.6)] {
let (b, want) = (occupied_bandwidth(sf, 0.99, span).unwrap(), former_occupied_bandwidth(sf, 0.99, span));
assert!((b - want).abs() < 1e-10, "SF {sf}: {b} vs {want}");
}
}
#[test]
fn the_exact_ser_is_that_of_a_simulated_receiver() {
let css = Css::lora(7).unwrap();
let mut rng = Rng::new(1);
let n = 30_000;
let s: Vec<usize> = (0..n).map(|_| rng.below(128)).collect();
let sigma = (1.0 / comnum_math::db2lin(-9.0)).sqrt();
let y: Vec<C64> =
css.modulate(&s).unwrap().into_iter().map(|v| v * cis(1.3) + rng.complex_gaussian() * sigma).collect();
let d = css.demodulate(&y).unwrap();
let sim = d.iter().zip(&s).filter(|(a, b)| a != b).count() as f64 / n as f64;
let want = ser(7, -9.0).unwrap();
assert!((sim / want - 1.0).abs() < 0.15, "{sim} vs {want}");
assert!((ber(7, -9.0).unwrap() / want - 128.0 / 254.0).abs() < 1e-12);
}
#[test]
fn the_cross_correlation_is_that_of_the_sampled_waveforms() {
let css =
Css { sf: 4, oversampling: 64, waveform: Waveform::LoRa, sample_rate: comnum_core::units::Hertz(1.0) };
let x: Vec<Vec<C64>> = (0..16).map(|a| css.symbol(a).unwrap()).collect();
let n = x[0].len() as f64;
for (l, q) in [(0usize, 1usize), (3, 7), (15, 2), (5, 13), (6, 6)] {
let c: C64 = x[l].iter().zip(&x[q]).map(|(a, b)| a * b.conj()).sum::<C64>() / n;
let want = cross_correlation(4, l, q).unwrap();
assert!((c - want).norm() < 0.02, "C({l}, {q}) = {c} vs {want}");
}
}
#[test]
fn the_spectrum_holds_the_power_and_the_lines_hold_one_over_m() {
let lines: f64 = spectral_lines(5, 40 << 5).unwrap().iter().map(|l| l.1).sum();
assert!((lines - 1.0 / 32.0).abs() < 1e-9, "{lines}");
let step = 1.0 / 256.0;
let f: Vec<f64> = (0..(4.0 / step) as usize).map(|i| -2.0 + (i as f64 + 0.5) * step).collect();
let cont: f64 = continuous_spectrum(5, &f).unwrap().iter().sum::<f64>() * step;
assert!((cont - (1.0 - 1.0 / 32.0)).abs() < 2e-3, "{cont}");
}
#[test]
fn the_closed_form_interference_is_the_dft_and_has_its_symmetries() {
let sorted = |mut v: Vec<f64>| {
v.sort_by(f64::total_cmp);
v
};
let mags = |s1: usize, s2: usize, tau: f64| {
interference_pattern(6, s1, s2, tau).unwrap().iter().map(|v| v.norm()).collect::<Vec<_>>()
};
for (s1, s2, tau) in [(5usize, 0usize, 3.0), (40, 10, 10.4), (63, 2, 30.7), (0, 0, 0.5), (17, 17, 62.9)] {
let dft = mags(s1, s2, tau);
let closed = interference_magnitudes(6, s1, s2, tau).unwrap();
let err = dft.iter().zip(&closed).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max);
assert!(err < 1e-9, "({s1}, {s2}, {tau}): {err}");
}
let base = sorted(mags(20, 5, 7.3));
for delta in [1usize, 10, 43] {
let shifted = sorted(mags(20 + delta, 5 + delta, 7.3));
assert!(base.iter().zip(&shifted).all(|(a, b)| (a - b).abs() < 1e-9), "δ = {delta}");
}
let (a, b) = (sorted(mags(20, 5, 7.3)), sorted(mags(20, 5, 63.0 - 7.3)));
assert!(a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9));
}
}
#[cfg(test)]
mod literature {
use super::*;
#[test]
fn chiani_2019_table_1() {
for (sf, corr, penalty) in [(3u32, 0.212, 1.04), (5, 0.091, 0.41), (7, 0.045, 0.20), (10, 0.015, 0.07)] {
let c = max_cross_correlation(sf).unwrap();
assert!((c - corr).abs() < 5e-4, "SF {sf}: {c}");
let p = -comnum_math::lin2db(1.0 - c);
assert!((p - penalty).abs() < 0.005, "SF {sf}: {p} dB");
}
for (sf, b99) in [(3u32, 1.500), (5, 1.185), (7, 1.045)] {
let b = occupied_bandwidth(sf, 0.99, 3.0).unwrap();
assert!((b / b99 - 1.0).abs() < 0.005, "SF {sf}: {b}");
let lines: f64 = spectral_lines(sf, 40 << sf).unwrap().iter().map(|l| l.1).sum();
assert!((lines * (1 << sf) as f64 - 1.0).abs() < 1e-4, "SF {sf}: {lines}");
}
}
#[test]
fn afisiadis_2020_fig_2_and_eqs_16_to_18() {
assert!((ser(7, -8.0).unwrap() / 1.8e-3 - 1.0).abs() < 0.15);
assert!((ser(7, -7.0).unwrap() / 1.4e-4 - 1.0).abs() < 0.1);
for sf in 7u32..=12 {
for snr in (-30..=0).map(f64::from) {
let exact = ser(sf, snr).unwrap();
if (1e-6..1e-2).contains(&exact) {
let (r, g) = (ser_rice(sf, snr).unwrap(), ser_gumbel(sf, snr).unwrap());
assert!((r / exact - 1.0).abs() < 0.3, "SF {sf} {snr} dB: {r} vs {exact}");
assert!(g < exact && g > exact / 6.0, "SF {sf} {snr} dB: {g} vs {exact}");
}
}
}
}
#[test]
fn vangelista_2017_fig_1_awgn() {
for (snr, read) in [(-10.0, 2e-2), (-9.0, 4.7e-3), (-8.0, 8e-4), (-7.0, 7.5e-5)] {
let b = ber(7, snr).unwrap();
assert!((b / read - 1.0).abs() < 0.15, "{snr} dB: {b} vs {read}");
}
}
#[test]
fn afisiadis_2020_eq_58_strongest_bin() {
let m = 128usize;
for (si, tau) in [(0usize, 3.0), (40, 10.4), (100, 30.7), (5, 50.2)] {
let r = interference_pattern(7, si, 0, tau).unwrap();
let kmax = comnum_math::argmax(r.iter().map(|v| v.norm())).unwrap().0;
assert_eq!(kmax, (m - tau.round() as usize) % m, "τ = {tau}");
assert!(r[tau.round() as usize].norm() < 0.05 * r[kmax].norm());
assert!((strongest_bin(m, si, tau) / r[kmax].norm() - 1.0).abs() < 5e-3);
}
}
}