pub mod theory;
use crate::{check_input, check_rate};
use comnum_core::signal::{Labels, Signal};
use comnum_core::traits::{Demodulate, Modulate};
use comnum_core::units::Hertz;
use comnum_core::{Result, ensure, ensure_shape};
use comnum_math::{C64, cis, fft};
use std::f64::consts::TAU;
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Waveform {
LoRa,
Vangelista,
Fsk,
}
impl std::str::FromStr for Waveform {
type Err = comnum_core::Error;
fn from_str(s: &str) -> Result<Waveform> {
match s.to_ascii_lowercase().as_str() {
"lora" => Ok(Waveform::LoRa),
"vangelista" => Ok(Waveform::Vangelista),
"fsk" => Ok(Waveform::Fsk),
_ => Err(comnum_core::Error::Config(format!("the waveform must be lora, vangelista or fsk, got {s:?}"))),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Css {
pub sf: u32,
pub oversampling: usize,
pub waveform: Waveform,
pub sample_rate: Hertz,
}
impl Css {
pub fn lora(sf: u32) -> Result<Css> {
let css = Css { sf, oversampling: 1, waveform: Waveform::LoRa, sample_rate: Hertz(1.0) };
css.check()?;
Ok(css)
}
pub fn m(&self) -> usize {
1 << self.sf
}
pub fn symbol_len(&self) -> usize {
self.oversampling * self.m()
}
pub fn check(&self) -> Result<()> {
ensure((1..=16).contains(&self.sf), || format!("the spreading factor must lie in 1..=16, got {}", self.sf))?;
check_rate(self.sample_rate)?;
ensure(self.oversampling >= 1, || "the oversampling factor must be at least 1".into())?;
ensure(self.waveform == Waveform::LoRa || self.oversampling == 1, || {
format!("the {:?} waveform is defined at one sample per chip", self.waveform)
})
}
fn phase(&self, a: usize, t: f64) -> f64 {
let m = self.m() as f64;
let a = a as f64;
match self.waveform {
Waveform::LoRa => {
let fold = if t >= m - a { 1.0 } else { 0.0 };
TAU * t * (a / m - 0.5 + t / (2.0 * m) - fold)
}
Waveform::Vangelista => TAU * (t * t + a * t) / m,
Waveform::Fsk => TAU * a * t / m,
}
}
pub fn symbol(&self, a: usize) -> Result<Vec<C64>> {
self.modulate(&[a])
}
pub fn modulate(&self, symbols: &[usize]) -> Result<Vec<C64>> {
self.check()?;
let m = self.m();
if let Some(a) = symbols.iter().find(|&&a| a >= m) {
return Err(comnum_core::Error::Config(format!("symbol {a} does not lie below M = {m}")));
}
let chirps = Chirps::new(self);
let mut out = Vec::with_capacity(symbols.len() * self.symbol_len());
for &a in symbols {
chirps.extend_symbol(&mut out, a);
}
Ok(out)
}
pub fn energies(&self, r: &[C64]) -> Result<Vec<f64>> {
self.check()?;
let (m, l) = (self.m(), self.symbol_len());
ensure_shape(r.len() % l == 0, || format!("{} samples is not a multiple of J M = {l}", r.len()))?;
let down: Vec<C64> = Chirps::new(self).base.iter().map(C64::conj).collect();
let mut out = Vec::with_capacity(r.len() / l * m);
for block in r.chunks(l) {
let y = fft::fft(&block.iter().zip(&down).map(|(a, b)| a * b).collect::<Vec<_>>());
if self.oversampling == 1 {
out.extend(y.iter().map(C64::norm_sqr));
} else {
out.extend((0..m).map(|k| y[k].norm_sqr() + y[(k + l - m) % l].norm_sqr()));
}
}
Ok(out)
}
pub fn demodulate(&self, r: &[C64]) -> Result<Vec<usize>> {
let m = self.m();
Ok(self.energies(r)?.chunks(m).map(|e| comnum_math::argmax(e.iter().copied()).map_or(0, |(k, _)| k)).collect())
}
pub fn interferer(&self, s1: usize, s2: usize, tau: f64) -> Result<Vec<C64>> {
self.check()?;
let m = self.m();
ensure(self.waveform == Waveform::LoRa && self.oversampling == 1, || {
"the interferer is a LoRa signal at one sample per chip".into()
})?;
ensure(s1 < m && s2 < m, || format!("the symbols must lie below M = {m}"))?;
ensure((0.0..m as f64).contains(&tau), || format!("the offset must lie in [0, {m}), got {tau}"))?;
let mf = m as f64;
let x = |t: f64, s: usize| cis(TAU * (t * t / (2.0 * mf) + t * (s as f64 / mf - 0.5)));
Ok((0..m)
.map(|n| {
let n = n as f64;
if n < tau.ceil() { x(n + mf - tau, s1) } else { x(n - tau, s2) }
})
.collect())
}
}
impl Modulate<Labels> for Css {
fn modulate_into(&self, x: &Labels, out: &mut Signal) -> Result<()> {
ensure(x.n_streams() == 1, || format!("CSS modulates one stream, not {}", x.n_streams()))?;
out.set_stream(&Css::modulate(self, x.stream(0))?, self.sample_rate);
Ok(())
}
}
impl Demodulate<Labels> for Css {
fn demodulate_into(&mut self, y: &Signal, out: &mut Labels) -> Result<()> {
check_input(y, self.sample_rate, "CSS")?;
out.set_stream(&Css::demodulate(self, y.stream(0))?);
Ok(())
}
}
struct Chirps {
base: Vec<C64>,
tone: Vec<C64>,
m: usize,
j: usize,
waveform: Waveform,
}
impl Chirps {
fn new(css: &Css) -> Chirps {
let (m, j, l) = (css.m(), css.oversampling, css.symbol_len());
let jf = j as f64;
let base = (0..l).map(|n| cis(css.phase(0, n as f64 / jf))).collect();
let tone = (0..l).map(|k| cis(TAU * k as f64 / l as f64)).collect();
Chirps { base, tone, m, j, waveform: css.waveform }
}
fn extend_symbol(&self, out: &mut Vec<C64>, a: usize) {
let l = self.base.len();
let fold = self.j * (self.m - a);
out.extend(self.base.iter().enumerate().map(|(n, b)| {
let v = b * self.tone[(a * n) % l];
if self.waveform == Waveform::LoRa && n >= fold { v * self.tone[(self.m * n) % l].conj() } else { v }
}));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_verb_traits_are_the_slice_methods() {
let mut rng = comnum_core::rng::Rng::new(7);
for (sf, oversampling, waveform) in
[(7u32, 1usize, Waveform::LoRa), (8, 4, Waveform::LoRa), (6, 1, Waveform::Fsk)]
{
let mut css = Css { sf, oversampling, waveform, sample_rate: Hertz(250e3) };
let a: Vec<usize> = (0..50).map(|_| rng.below(css.m())).collect();
let s = css.modulate(&a).unwrap();
let mut x = Signal::zeros(0, 0, Hertz(1.0));
css.modulate_into(&Labels::from_stream(a.clone()).unwrap(), &mut x).unwrap();
assert_eq!((x.stream(0), x.sample_rate()), (&s[..], Hertz(250e3)));
let noisy: Vec<C64> = s.iter().map(|v| v + C64::new(rng.gaussian(), rng.gaussian()) * 3.0).collect();
let y = Signal::from_stream(noisy.clone(), Hertz(250e3)).unwrap();
let mut out = Labels::default();
css.demodulate_into(&y, &mut out).unwrap();
assert_eq!(out.stream(0), css.demodulate(&noisy).unwrap());
assert!(css.demodulate_into(&Signal::from_stream(noisy, Hertz(1.0)).unwrap(), &mut out).is_err());
assert!(Modulate::modulate(&css, &Labels::zeros(2, 4)).is_err());
}
assert!(Css { sample_rate: Hertz(0.0), ..Css::lora(7).unwrap() }.check().is_err());
}
#[test]
fn the_tabulated_chirps_are_the_direct_ones() {
for (sf, oversampling, waveform) in [
(7u32, 1usize, Waveform::LoRa),
(7, 4, Waveform::LoRa),
(12, 1, Waveform::LoRa),
(9, 8, Waveform::LoRa),
(7, 1, Waveform::Vangelista),
(10, 1, Waveform::Vangelista),
(7, 1, Waveform::Fsk),
] {
let css = Css { sf, oversampling, waveform, sample_rate: Hertz(1.0) };
let m = css.m();
let jf = oversampling as f64;
let symbols: Vec<usize> = (0..m).step_by((m / 64).max(1)).chain([m - 1]).collect();
let fast = css.modulate(&symbols).unwrap();
for (i, &a) in symbols.iter().enumerate() {
let direct: Vec<C64> = (0..css.symbol_len()).map(|n| cis(css.phase(a, n as f64 / jf))).collect();
let got = &fast[i * css.symbol_len()..(i + 1) * css.symbol_len()];
let err = got.iter().zip(&direct).map(|(x, y)| (x - y).norm()).fold(0.0, f64::max);
assert!(err < 1e-11, "SF {sf} J {oversampling} {waveform:?} symbol {a}: {err}");
assert!(a != 0 || err == 0.0, "{waveform:?} symbol 0: {err}");
}
}
assert!(Css::lora(7).unwrap().modulate(&[3, 128]).is_err());
for sf in [7u32, 10, 12] {
let css = Css::lora(sf).unwrap();
let m = css.m();
let (mut direct, mut tabulated) = (0.0f64, 0.0f64);
for a in (0..m).step_by(m / 32) {
let fast = css.symbol(a).unwrap();
for (n, v) in fast.iter().enumerate() {
let fold = usize::from(n >= m - a);
let num = (n * n + 2 * a * n + 4 * m * n - m * n - 2 * m * n * fold) % (2 * m);
let exact = cis(TAU * num as f64 / (2 * m) as f64);
direct = direct.max((cis(css.phase(a, n as f64)) - exact).norm());
tabulated = tabulated.max((v - exact).norm());
}
}
assert!(tabulated < direct, "SF {sf}: tabulated {tabulated:e}, direct {direct:e}");
}
}
#[test]
fn the_waveforms_are_orthogonal_and_the_fft_receiver_is_the_correlator_bank() {
for waveform in [Waveform::LoRa, Waveform::Vangelista, Waveform::Fsk] {
let css = Css { sf: 5, oversampling: 1, waveform, sample_rate: Hertz(1.0) };
let m = css.m();
let x: Vec<Vec<C64>> = (0..m).map(|a| css.symbol(a).unwrap()).collect();
for i in 0..m {
for q in 0..m {
let c: C64 = x[i].iter().zip(&x[q]).map(|(a, b)| a * b.conj()).sum::<C64>() / m as f64;
let want = if i == q { 1.0 } else { 0.0 };
assert!((c - want).norm() < 1e-12, "{waveform:?}: <{i}, {q}> = {c}");
}
}
let r: Vec<C64> = (0..m).map(|n| C64::new((0.3 * n as f64).sin(), (1.7 * n as f64).cos())).collect();
let e = css.energies(&r).unwrap();
for (q, xq) in x.iter().enumerate() {
let c: C64 = r.iter().zip(xq).map(|(a, b)| a * b.conj()).sum();
assert!((c.norm_sqr() - e[q]).abs() < 1e-9, "{waveform:?}: bin {q}");
}
}
}
#[test]
fn lora_is_a_continuous_phase_chirp_sampled_at_any_rate() {
let css = Css { sf: 6, oversampling: 8, waveform: Waveform::LoRa, sample_rate: Hertz(1.0) };
let m = css.m() as f64;
for a in [0usize, 1, 17, 63] {
let end = css.phase(a, m - 1e-9).rem_euclid(TAU);
assert!(end.min(TAU - end) < 1e-6, "symbol {a}: {end}");
let x = css.symbol(a).unwrap();
let df: Vec<f64> = x.windows(2).map(|w| (w[1] * w[0].conj()).arg() * 8.0 / TAU).collect();
assert!((df[0] - (a as f64 / m - 0.5)).abs() < 0.02, "{a}: {}", df[0]);
}
let s = css.modulate(&[3, 0, 63, 40]).unwrap();
assert_eq!(css.demodulate(&s).unwrap(), vec![3, 0, 63, 40]);
let one = Css::lora(6).unwrap();
let x = one.symbol(9).unwrap();
for (n, v) in x.iter().enumerate() {
let n = n as f64;
let want = cis(TAU * n * (9.0 / 64.0 - 0.5 + n / 128.0));
assert!((v - want).norm() < 1e-9);
}
assert!(Css { sf: 6, oversampling: 2, waveform: Waveform::Fsk, sample_rate: Hertz(1.0) }.symbol(0).is_err());
assert!(one.symbol(64).is_err() && Css::lora(0).is_err());
}
#[test]
fn the_interferer_is_the_lora_signal_shifted_by_an_integer_offset() {
let css = Css::lora(5).unwrap();
let stream = css.modulate(&[7, 20]).unwrap();
for tau in [0usize, 1, 13, 31] {
let y = css.interferer(7, 20, tau as f64).unwrap();
for (n, v) in y.iter().enumerate() {
assert!((v - stream[n + 32 - tau]).norm() < 1e-9, "τ = {tau}, n = {n}");
}
}
assert!(css.interferer(0, 0, 32.0).is_err());
}
}