1use super::Css;
39use comnum_core::Result;
40use comnum_math::{C64, cis, db2lin, fft, fresnel, qfunc};
41use std::f64::consts::{PI, TAU};
42
43fn chips(sf: u32) -> Result<usize> {
48 Css::lora(sf)?;
49 Ok(1 << sf)
50}
51
52pub fn ser(sf: u32, snr_db: f64) -> Result<f64> {
68 let m = chips(sf)?;
69 comnum_mapping::theory::ser_orthogonal_noncoherent(m, snr_db + comnum_math::lin2db(m as f64))
70}
71
72pub fn ber(sf: u32, snr_db: f64) -> Result<f64> {
79 let m = chips(sf)?;
80 comnum_mapping::theory::ber_orthogonal_noncoherent(m, snr_db + comnum_math::lin2db(m as f64))
81}
82
83fn harmonic(n: usize) -> f64 {
85 (1..=n).map(|k| 1.0 / k as f64).sum()
86}
87
88pub fn ser_rice(sf: u32, snr_db: f64) -> Result<f64> {
96 let m = chips(sf)?;
97 let h = harmonic(m - 1);
98 let d = (h * h - PI * PI / 12.0).sqrt();
99 let gamma = m as f64 * db2lin(snr_db);
100 Ok(qfunc((gamma.sqrt() - d.sqrt()) / (h - d + 0.5).sqrt()))
101}
102
103pub fn ser_gumbel(sf: u32, snr_db: f64) -> Result<f64> {
110 const EULER: f64 = 0.577_215_664_901_532_9;
111 let gamma = chips(sf)? as f64 * db2lin(snr_db);
112 Ok(qfunc((2.0 * gamma).sqrt() - (2.0 * (std::f64::consts::LN_2 * f64::from(sf) + EULER)).sqrt()))
113}
114
115pub fn cross_correlation(sf: u32, l: usize, m_sym: usize) -> Result<C64> {
123 let m = chips(sf)?;
124 comnum_core::ensure(l < m && m_sym < m, || format!("the symbols {l} and {m_sym} must lie below M = {m}"))?;
125 Ok(correlation(m as f64, l, m_sym))
126}
127
128fn correlation(m: f64, l: usize, m_sym: usize) -> C64 {
130 if l == m_sym {
131 return C64::new(1.0, 0.0);
132 }
133 let (l, q) = (l as f64, m_sym as f64);
134 let d = (q - l).abs();
135 let num = cis(TAU * l * (q - l) / m) - cis(TAU * q * (q - l) / m);
136 num * m / (C64::new(0.0, TAU) * (m - d) * d)
137}
138
139pub fn max_cross_correlation(sf: u32) -> Result<f64> {
147 let m = chips(sf)?;
148 let mut best = f64::NEG_INFINITY;
150 for l in 0..m {
151 for q in 0..m {
152 if l != q {
153 best = best.max(correlation(m as f64, l, q).re);
154 }
155 }
156 }
157 Ok(best)
158}
159
160fn fresnel_k(x: f64) -> C64 {
162 let (c, s) = fresnel(x);
163 C64::new(c, s)
164}
165
166struct Shared {
180 m: f64,
182 c: f64,
184 d: C64,
186 w1: C64,
188 root: f64,
190}
191
192fn chirp_phase(m: usize, x: f64) -> C64 {
194 cis(-PI * x * x / m as f64)
195}
196
197impl Shared {
198 fn new(m: usize, f: f64) -> Shared {
199 let m = m as f64;
200 let s = (2.0 / m).sqrt();
201 let w = cis(-TAU * f * m);
202 let d = fresnel_k(s * (m / 2.0 - f * m)) - w * fresnel_k(s * (-m / 2.0 - f * m));
203 Shared { m, c: m / 2.0 + f * m, d, w1: w - 1.0, root: (m / 2.0).sqrt() }
204 }
205
206 fn spectrum(&self, phase: C64, k: C64) -> C64 {
209 phase * (self.d + self.w1 * k) * self.root
210 }
211
212 fn density(&self, terms: impl Iterator<Item = C64>) -> f64 {
216 let (energy, sum) = terms.fold((0.0, C64::new(0.0, 0.0)), |(e, s), x| (e + x.norm_sqr(), s + x));
217 (energy - sum.norm_sqr() / self.m) / (self.m * self.m)
218 }
219}
220
221pub fn continuous_spectrum(sf: u32, f: &[f64]) -> Result<Vec<f64>> {
231 let m = chips(sf)?;
232 let s = (2.0 / m as f64).sqrt();
233 Ok(f.iter()
234 .map(|&fk| {
235 let shared = Shared::new(m, fk);
236 shared.density((0..m).map(|l| {
237 let x = l as f64 - shared.c;
238 shared.spectrum(chirp_phase(m, x), fresnel_k(s * x))
239 }))
240 })
241 .collect())
242}
243
244fn continuous_spectrum_grid(m: usize, n: usize) -> Vec<f64> {
250 let s = (2.0 / m as f64).sqrt();
251 let mf = m as f64;
252 let table: Vec<(C64, C64)> = (0..8 * (m - 1) + n)
254 .map(|j| {
255 let x = (j as f64 - (n - 1) as f64 - 4.0 * mf - 0.5) / 8.0;
256 (chirp_phase(m, x), fresnel_k(s * x))
257 })
258 .collect();
259 (0..n)
260 .map(|i| {
261 let shared = Shared::new(m, (i as f64 + 0.5) / (8.0 * mf));
262 shared.density((0..m).map(|l| {
263 let (phase, k) = table[8 * l + n - 1 - i];
264 shared.spectrum(phase, k)
265 }))
266 })
267 .collect()
268}
269
270pub fn spectral_lines(sf: u32, n_max: usize) -> Result<Vec<(f64, f64)>> {
288 let m = chips(sf)?;
289 let mf = m as f64;
290 let s = (2.0 / mf).sqrt();
291 let n_max = n_max as i64;
292 Ok((-n_max..=n_max)
293 .map(|n| {
294 let n = n as f64;
295 let d = fresnel_k(s * (mf / 2.0 - n)) + fresnel_k(s * (mf / 2.0 + n));
296 (n / mf, d.norm_sqr() / (2.0 * mf * mf))
297 })
298 .collect())
299}
300
301pub fn occupied_bandwidth(sf: u32, fraction: f64, span: f64) -> Result<f64> {
313 let m = chips(sf)?;
314 comnum_core::ensure(fraction > 0.0 && fraction < 1.0 && span > 0.0, || {
315 format!("a fraction in (0, 1) and a positive span, got {fraction} and {span}")
316 })?;
317 let step = 1.0 / (8.0 * m as f64);
318 let n = (span / step).ceil() as usize;
319 let g = continuous_spectrum_grid(m, n);
321 let mut lines = spectral_lines(sf, (span * m as f64).ceil() as usize)?;
322 let mut inside = lines.iter().filter(|(fl, _)| *fl == 0.0).map(|l| l.1).sum::<f64>();
324 lines.sort_by(|a, b| a.0.abs().total_cmp(&b.0.abs()));
326 let (mut lo, mut hi) = (0, 0);
327 let mut prev = (0.0, inside);
328 for (i, gi) in g.iter().enumerate() {
329 let z = (i + 1) as f64 * step;
330 inside += 2.0 * gi * step;
331 while lo < lines.len() && lines[lo].0.abs() <= z - step {
332 lo += 1;
333 }
334 hi = hi.max(lo);
335 while hi < lines.len() && lines[hi].0.abs() <= z {
336 hi += 1;
337 }
338 inside += lines[lo..hi].iter().map(|l| l.1).sum::<f64>();
339 if inside >= fraction {
340 let (z0, p0) = prev;
341 return Ok(2.0 * (z0 + (z - z0) * (fraction - p0) / (inside - p0)));
342 }
343 prev = (z, inside);
344 }
345 Ok(2.0 * span)
346}
347
348pub fn interference_pattern(sf: u32, s1: usize, s2: usize, tau: f64) -> Result<Vec<C64>> {
355 let css = Css::lora(sf)?;
356 let x = css.interferer(s1, s2, tau)?;
357 let reference = css.symbol(0)?;
358 Ok(fft::fft(&x.iter().zip(&reference).map(|(a, b)| a * b.conj()).collect::<Vec<_>>()))
359}
360
361pub fn interference_magnitudes(sf: u32, s1: usize, s2: usize, tau: f64) -> Result<Vec<f64>> {
369 let m = chips(sf)?;
370 comnum_core::ensure(s1 < m && s2 < m, || format!("the symbols must lie below M = {m}"))?;
371 comnum_core::ensure((0.0..m as f64).contains(&tau), || format!("the offset must lie in [0, {m}), got {tau}"))?;
372 let mf = m as f64;
373 let c = tau.ceil();
374 let (l_int, lambda) = (tau.floor(), tau - tau.floor());
375 let ratio = |x: f64, len: f64| sine_ratio(x, len, mf);
376 let (s1f, s2f) = (s1 as f64, s2 as f64);
377 Ok((0..m)
378 .map(|k| {
379 let k = k as f64;
380 let a1 = ratio(s1f - k - tau, c);
381 let a2 = ratio(s2f - k - tau, mf - c);
382 let t1 = PI / mf
383 * (-tau * tau + (lambda - l_int) * mf + s1f * (2.0 * tau - c + 1.0) + k * (c - 1.0) + tau * (c - 1.0));
384 let t2 = PI / mf * (-tau * tau + s2f * (2.0 * tau - c + 1.0 - mf) + k * (c - 1.0 + mf) + tau * (c - 1.0));
385 (a1 * a1 + a2 * a2 + 2.0 * a1 * a2 * (t1 - t2).cos()).max(0.0).sqrt()
386 })
387 .collect())
388}
389
390fn sine_ratio(x: f64, len: f64, m: f64) -> f64 {
394 if len == 0.0 { 0.0 } else { len * comnum_math::diric(TAU * x / m, len as usize) }
395}
396
397fn strongest_bin(m: usize, si: usize, tau: f64) -> f64 {
402 let mf = m as f64;
403 let c = tau.ceil();
404 let k = -tau.round();
405 (sine_ratio(si as f64 - k - tau, c, mf) + sine_ratio(-k - tau, mf - c, mf)).abs()
406}
407
408fn interference_error(m: usize, si: usize, tau: f64, rho: f64, pi_lin: f64) -> f64 {
413 let mf = m as f64;
414 qfunc((mf - pi_lin.sqrt() * strongest_bin(m, si, tau)) / (mf / rho).sqrt())
415}
416
417const PAPER_OFFSET_STEP: f64 = 0.2;
419
420const MIN_OFFSET_STEP: f64 = 1e-3;
422
423fn offsets(half: f64, eps: f64) -> (f64, Vec<f64>) {
426 let eps = if eps.is_finite() { eps.max(MIN_OFFSET_STEP) } else { PAPER_OFFSET_STEP };
427 let n = (half / eps).ceil().max(0.0) as usize;
428 (eps, (0..n).map(|i| i as f64 * eps).filter(|t| *t < half).collect())
429}
430
431pub fn ser_interference(sf: u32, snr_db: f64, pi_db: f64, eps: f64) -> Result<f64> {
439 let m = chips(sf)?;
440 let (r, pi) = (db2lin(snr_db), comnum_math::db2lin(pi_db));
441 let half = (m as f64 - 1.0) / 2.0;
442 let (eps, taus) = offsets(half, eps);
443 let mut acc = 0.0;
444 for si in 0..m {
445 for &tau in &taus {
446 acc += interference_error(m, si, tau, r, pi);
447 }
448 }
449 let p_int = acc * eps / (m as f64 * (m as f64 / 2.0));
450 let p_noise = ser(sf, snr_db)?;
451 Ok(p_noise + (1.0 - p_noise) * p_int)
452}
453
454pub fn fer_interference(sf: u32, snr_db: f64, pi_db: f64, frame: usize, eps: f64) -> Result<f64> {
463 let m = chips(sf)?;
464 let (r, pi) = (db2lin(snr_db), comnum_math::db2lin(pi_db));
465 let half = (m as f64 - 1.0) / 2.0;
466 let (eps, taus) = offsets(half, eps);
467 let p_tau: Vec<f64> = taus
469 .iter()
470 .map(|&tau| (0..m).map(|si| interference_error(m, si, tau, r, pi)).sum::<f64>() / m as f64)
471 .collect();
472 let p_noise = ser(sf, snr_db)?;
473 let mut total = 0.0;
474 for fi in 1..=frame {
475 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();
477 total += 2.0 / m as f64 * eps * (taus.len() as f64 - ok);
478 }
479 Ok((total / frame as f64).clamp(0.0, 1.0))
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::css::Waveform;
486 use comnum_core::rng::Rng;
487
488 fn former_waveform_spectrum(m: usize, l: usize, f: f64) -> C64 {
491 let segment = |a: f64, b: f64, t1: f64, t2: f64| {
492 let r = b.sqrt();
493 let shift = a / (2.0 * b);
494 cis(-TAU * a * a / (4.0 * b)) / (2.0 * r)
495 * (fresnel_k(2.0 * r * (t2 + shift)) - fresnel_k(2.0 * r * (t1 + shift)))
496 };
497 let (mf, lf) = (m as f64, l as f64);
498 let b = 1.0 / (2.0 * mf);
499 let fold = mf - lf;
500 segment(lf / mf - 0.5 - f, b, 0.0, fold) + segment(lf / mf - 1.5 - f, b, fold, mf)
501 }
502
503 fn former_continuous_spectrum(m: usize, f: f64) -> f64 {
504 let mf = m as f64;
505 let x: Vec<C64> = (0..m).map(|l| former_waveform_spectrum(m, l, f)).collect();
506 let sum: C64 = x.iter().sum();
507 (x.iter().map(C64::norm_sqr).sum::<f64>() - sum.norm_sqr() / mf) / (mf * mf)
508 }
509
510 fn former_occupied_bandwidth(sf: u32, fraction: f64, span: f64) -> f64 {
513 let m = 1usize << sf;
514 let mf = m as f64;
515 let step = 1.0 / (8.0 * mf);
516 let n = (span / step).ceil() as usize;
517 let g: Vec<f64> = (0..n).map(|i| former_continuous_spectrum(m, (i as f64 + 0.5) * step)).collect();
518 let n_max = (span * mf).ceil() as i64;
519 let lines: Vec<(f64, f64)> = (-n_max..=n_max)
520 .map(|k| {
521 let f = k as f64 / mf;
522 let sum: C64 = (0..m).map(|l| former_waveform_spectrum(m, l, f)).sum();
523 (f, sum.norm_sqr() / (mf * mf * mf * mf))
524 })
525 .collect();
526 let mut inside = lines.iter().filter(|(fl, _)| *fl == 0.0).map(|l| l.1).sum::<f64>();
527 let mut prev = (0.0, inside);
528 for (i, gi) in g.iter().enumerate() {
529 let z = (i + 1) as f64 * step;
530 inside += 2.0 * gi * step;
531 inside += lines.iter().filter(|(fl, _)| fl.abs() > z - step && fl.abs() <= z).map(|l| l.1).sum::<f64>();
532 if inside >= fraction {
533 let (z0, p0) = prev;
534 return 2.0 * (z0 + (z - z0) * (fraction - p0) / (inside - p0));
535 }
536 prev = (z, inside);
537 }
538 2.0 * span
539 }
540
541 #[test]
542 fn the_spectrum_is_the_former_evaluation_of_eq_25() {
543 let mut rng = Rng::new(9);
544 for sf in [1u32, 3, 5, 8] {
545 let m = 1usize << sf;
546 let f: Vec<f64> = (0..40).map(|_| 3.0 * rng.uniform() - 1.5).collect();
548 let peak = former_continuous_spectrum(m, 0.0).max(1e-3);
549 for (fk, g) in f.iter().zip(continuous_spectrum(sf, &f).unwrap()) {
550 let want = former_continuous_spectrum(m, *fk);
551 assert!((g - want).abs() < 1e-12 * peak, "SF {sf}, f {fk}: {g} vs {want}");
552 }
553 let n = 8 * m + 5;
554 let mid: Vec<f64> = (0..n).map(|i| (i as f64 + 0.5) / (8.0 * m as f64)).collect();
555 let direct = continuous_spectrum(sf, &mid).unwrap();
556 for (a, b) in continuous_spectrum_grid(m, n).iter().zip(&direct) {
557 assert!((a - b).abs() < 1e-12 * peak, "SF {sf}: {a} vs {b}");
558 }
559 for (fl, p) in spectral_lines(sf, 3 * m).unwrap() {
560 let sum: C64 = (0..m).map(|l| former_waveform_spectrum(m, l, fl)).sum();
561 let want = sum.norm_sqr() / (m as f64).powi(4);
562 assert!((p - want).abs() < 1e-12 / m as f64, "SF {sf}, line {fl}: {p} vs {want}");
563 }
564 }
565 for (sf, span) in [(3u32, 3.0), (5, 3.0), (7, 0.6)] {
566 let (b, want) = (occupied_bandwidth(sf, 0.99, span).unwrap(), former_occupied_bandwidth(sf, 0.99, span));
567 assert!((b - want).abs() < 1e-10, "SF {sf}: {b} vs {want}");
568 }
569 }
570
571 #[test]
572 fn the_exact_ser_is_that_of_a_simulated_receiver() {
573 let css = Css::lora(7).unwrap();
575 let mut rng = Rng::new(1);
576 let n = 30_000;
577 let s: Vec<usize> = (0..n).map(|_| rng.below(128)).collect();
578 let sigma = (1.0 / comnum_math::db2lin(-9.0)).sqrt();
579 let y: Vec<C64> =
580 css.modulate(&s).unwrap().into_iter().map(|v| v * cis(1.3) + rng.complex_gaussian() * sigma).collect();
581 let d = css.demodulate(&y).unwrap();
582 let sim = d.iter().zip(&s).filter(|(a, b)| a != b).count() as f64 / n as f64;
583 let want = ser(7, -9.0).unwrap();
584 assert!((sim / want - 1.0).abs() < 0.15, "{sim} vs {want}");
585 assert!((ber(7, -9.0).unwrap() / want - 128.0 / 254.0).abs() < 1e-12);
586 }
587
588 #[test]
589 fn the_cross_correlation_is_that_of_the_sampled_waveforms() {
590 let css =
593 Css { sf: 4, oversampling: 64, waveform: Waveform::LoRa, sample_rate: comnum_core::units::Hertz(1.0) };
594 let x: Vec<Vec<C64>> = (0..16).map(|a| css.symbol(a).unwrap()).collect();
595 let n = x[0].len() as f64;
596 for (l, q) in [(0usize, 1usize), (3, 7), (15, 2), (5, 13), (6, 6)] {
597 let c: C64 = x[l].iter().zip(&x[q]).map(|(a, b)| a * b.conj()).sum::<C64>() / n;
598 let want = cross_correlation(4, l, q).unwrap();
599 assert!((c - want).norm() < 0.02, "C({l}, {q}) = {c} vs {want}");
600 }
601 }
602
603 #[test]
604 fn the_spectrum_holds_the_power_and_the_lines_hold_one_over_m() {
605 let lines: f64 = spectral_lines(5, 40 << 5).unwrap().iter().map(|l| l.1).sum();
607 assert!((lines - 1.0 / 32.0).abs() < 1e-9, "{lines}");
608 let step = 1.0 / 256.0;
609 let f: Vec<f64> = (0..(4.0 / step) as usize).map(|i| -2.0 + (i as f64 + 0.5) * step).collect();
610 let cont: f64 = continuous_spectrum(5, &f).unwrap().iter().sum::<f64>() * step;
611 assert!((cont - (1.0 - 1.0 / 32.0)).abs() < 2e-3, "{cont}");
612 }
613
614 #[test]
615 fn the_closed_form_interference_is_the_dft_and_has_its_symmetries() {
616 let sorted = |mut v: Vec<f64>| {
618 v.sort_by(f64::total_cmp);
619 v
620 };
621 let mags = |s1: usize, s2: usize, tau: f64| {
622 interference_pattern(6, s1, s2, tau).unwrap().iter().map(|v| v.norm()).collect::<Vec<_>>()
623 };
624 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)] {
625 let dft = mags(s1, s2, tau);
626 let closed = interference_magnitudes(6, s1, s2, tau).unwrap();
627 let err = dft.iter().zip(&closed).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max);
628 assert!(err < 1e-9, "({s1}, {s2}, {tau}): {err}");
629 }
630 let base = sorted(mags(20, 5, 7.3));
633 for delta in [1usize, 10, 43] {
634 let shifted = sorted(mags(20 + delta, 5 + delta, 7.3));
635 assert!(base.iter().zip(&shifted).all(|(a, b)| (a - b).abs() < 1e-9), "δ = {delta}");
636 }
637 let (a, b) = (sorted(mags(20, 5, 7.3)), sorted(mags(20, 5, 63.0 - 7.3)));
639 assert!(a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9));
640 }
641}
642
643#[cfg(test)]
644mod literature {
645 use super::*;
646
647 #[test]
654 fn chiani_2019_table_1() {
655 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)] {
656 let c = max_cross_correlation(sf).unwrap();
657 assert!((c - corr).abs() < 5e-4, "SF {sf}: {c}");
658 let p = -comnum_math::lin2db(1.0 - c);
659 assert!((p - penalty).abs() < 0.005, "SF {sf}: {p} dB");
660 }
661 for (sf, b99) in [(3u32, 1.500), (5, 1.185), (7, 1.045)] {
662 let b = occupied_bandwidth(sf, 0.99, 3.0).unwrap();
663 assert!((b / b99 - 1.0).abs() < 0.005, "SF {sf}: {b}");
664 let lines: f64 = spectral_lines(sf, 40 << sf).unwrap().iter().map(|l| l.1).sum();
666 assert!((lines * (1 << sf) as f64 - 1.0).abs() < 1e-4, "SF {sf}: {lines}");
667 }
668 }
669
670 #[test]
679 fn afisiadis_2020_fig_2_and_eqs_16_to_18() {
680 assert!((ser(7, -8.0).unwrap() / 1.8e-3 - 1.0).abs() < 0.15);
681 assert!((ser(7, -7.0).unwrap() / 1.4e-4 - 1.0).abs() < 0.1);
682 for sf in 7u32..=12 {
683 for snr in (-30..=0).map(f64::from) {
684 let exact = ser(sf, snr).unwrap();
685 if (1e-6..1e-2).contains(&exact) {
686 let (r, g) = (ser_rice(sf, snr).unwrap(), ser_gumbel(sf, snr).unwrap());
687 assert!((r / exact - 1.0).abs() < 0.3, "SF {sf} {snr} dB: {r} vs {exact}");
688 assert!(g < exact && g > exact / 6.0, "SF {sf} {snr} dB: {g} vs {exact}");
689 }
690 }
691 }
692 }
693
694 #[test]
699 fn vangelista_2017_fig_1_awgn() {
700 for (snr, read) in [(-10.0, 2e-2), (-9.0, 4.7e-3), (-8.0, 8e-4), (-7.0, 7.5e-5)] {
701 let b = ber(7, snr).unwrap();
702 assert!((b / read - 1.0).abs() < 0.15, "{snr} dB: {b} vs {read}");
703 }
704 }
705
706 #[test]
710 fn afisiadis_2020_eq_58_strongest_bin() {
711 let m = 128usize;
712 for (si, tau) in [(0usize, 3.0), (40, 10.4), (100, 30.7), (5, 50.2)] {
713 let r = interference_pattern(7, si, 0, tau).unwrap();
714 let kmax = comnum_math::argmax(r.iter().map(|v| v.norm())).unwrap().0;
715 assert_eq!(kmax, (m - tau.round() as usize) % m, "τ = {tau}");
716 assert!(r[tau.round() as usize].norm() < 0.05 * r[kmax].norm());
717 assert!((strongest_bin(m, si, tau) / r[kmax].norm() - 1.0).abs() < 5e-3);
719 }
720 }
721}