comnum_waveform/css/mod.rs
1//! Chirp spread spectrum: the frequency-shift chirp modulation of LoRa.
2//!
3//! # Model
4//!
5//! A symbol $a \in \{0, \dots, M - 1\}$, $M = 2^{\mathrm{SF}}$ with the spreading factor
6//! $\mathrm{SF}$, is a chirp of $M$ chips over the bandwidth $B$ whose initial frequency
7//! is shifted by $aB/M$ and folded at $B/2$. Its complex envelope over
8//! $0 \le t < T_{s} = M/B$ is (M. Chiani and A. Elzanaty, *IEEE Internet Things J.*
9//! 6(5), 2019, eq. (5))
10//!
11//! $$x(t; a) = \exp\left\\{j 2\pi B t \left(\frac{a}{M} - \frac{1}{2} + \frac{Bt}{2M} -
12//! u\left(t - \frac{M - a}{B}\right)\right)\right\\},$$
13//!
14//! a memoryless continuous-phase modulation. At one sample per chip the fold vanishes
15//! and $x_{a}\[n\] = e^{j2\pi(n^{2}/(2M) + (a/M - 1/2) n)}$ (their eq. (13); eq. (1) of
16//! Tapparel et al. 2020, eq. (2) of Afisiadis et al. 2020). [`Css`] samples it
17//! `oversampling` times per chip; [`Waveform`] also gives, for comparison, the discrete
18//! chirp of L. Vangelista, *IEEE Signal Process. Lett.* 24(12), 2017, eq. (3),
19//! $e^{j2\pi((a + n) \bmod M) n/M}$, whose quadratic phase lacks the factor $1/2$
20//! (Chiani and Elzanaty, Sec. II-A), and the FSK of the same cardinality of his eq. (17).
21//!
22//! The modulator tabulates the chirp of symbol 0 and the tones $e^{j 2\pi k/(JM)}$ once
23//! per call: the sample $n$ of symbol $a$ is the product of the base chirp, the tone
24//! $(a n) \bmod JM$ and, after the fold, the conjugate of the tone $(M n) \bmod JM$,
25//! within a few units of the last place of the direct evaluation of the phase.
26//!
27//! # Demodulation
28//!
29//! The receiver multiplies every symbol by the conjugate of the chirp of symbol 0 and
30//! takes a DFT (Vangelista, eq. (16)): the dechirped symbol $a$ is a tone in bin $a$
31//! (and, when oversampled, in bin $a - M$ after the fold), so the noncoherent decision
32//! is the bin of largest energy, the maximum-likelihood detection of these orthogonal
33//! signals with an unknown phase. [`theory`] gives its error rates, the spectrum and
34//! the effect of a same-SF interferer.
35//!
36//! # SNR
37//!
38//! The SNR is that of a sample at one sample per chip (the signal power over the noise
39//! power in the band $B$), the abscissa of the figures of Vangelista (2017), Afisiadis
40//! et al. (2020) and Tapparel et al. (2020); the SNR per symbol is $M$ times larger.
41//!
42//! # Trademarks and patents
43//!
44//! LoRa is a trademark of Semtech Corporation, and the modulation is covered by patents
45//! (F. Sforza, US 8,406,275, 2013; O. B. A. Seller and N. Sornin, US 9,252,834, 2016).
46//! This module models the published descriptions above for simulation and research; it
47//! is not the code of any vendor nor a certified implementation.
48//!
49//! # Verb traits
50//!
51//! [`Css`] implements `Modulate<Labels>` and `Demodulate<Labels>` of `comnum-core` on one
52//! stream of symbol indices ([`Labels`]), at its [`Css::sample_rate`] (1 Hz by default,
53//! time in samples), the demodulator refusing an input at another rate: the slice methods
54//! [`Css::modulate`] and [`Css::demodulate`] (the noncoherent decisions) inside. The soft
55//! output, [`Css::energies`], stays a method.
56//!
57//! # Examples
58//! ```
59//! use comnum_core::prelude::*;
60//! use comnum_waveform::css::Css;
61//! let css = Css::lora(7)?;
62//! let s = css.modulate(&[0, 5, 127])?;
63//! assert_eq!(s.len(), 3 * 128);
64//! assert!(s.iter().all(|v| (v.norm() - 1.0).abs() < 1e-12)); // constant envelope
65//! assert_eq!(css.demodulate(&s)?, vec![0, 5, 127]);
66//! // the same through the verb traits
67//! let mut css = Css { sample_rate: Hertz(125e3), ..css };
68//! let x = Modulate::modulate(&css, &Labels::from_stream(vec![0, 5, 127])?)?;
69//! assert_eq!((x.stream(0), x.sample_rate()), (&s[..], Hertz(125e3)));
70//! let a: Labels = Demodulate::demodulate(&mut css, &x)?;
71//! assert_eq!(a.stream(0), [0, 5, 127]);
72//! # Ok::<(), comnum_core::Error>(())
73//! ```
74
75pub mod theory;
76
77use crate::{check_input, check_rate};
78use comnum_core::signal::{Labels, Signal};
79use comnum_core::traits::{Demodulate, Modulate};
80use comnum_core::units::Hertz;
81use comnum_core::{Result, ensure, ensure_shape};
82use comnum_math::{C64, cis, fft};
83use std::f64::consts::TAU;
84
85/// The family of waveforms (see the module).
86#[non_exhaustive]
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Waveform {
89 /// The chirp of LoRa (Chiani and Elzanaty 2019, eq. (5)), at any oversampling.
90 LoRa,
91 /// The discrete chirp of Vangelista (2017), eq. (3), one sample per chip.
92 Vangelista,
93 /// The FSK of the same cardinality, Vangelista (2017), eq. (17), one sample per chip.
94 Fsk,
95}
96
97impl std::str::FromStr for Waveform {
98 type Err = comnum_core::Error;
99
100 /// `lora`, `vangelista` or `fsk` (case-insensitive).
101 ///
102 /// # Errors
103 /// [`comnum_core::Error::Config`] for another name.
104 fn from_str(s: &str) -> Result<Waveform> {
105 match s.to_ascii_lowercase().as_str() {
106 "lora" => Ok(Waveform::LoRa),
107 "vangelista" => Ok(Waveform::Vangelista),
108 "fsk" => Ok(Waveform::Fsk),
109 _ => Err(comnum_core::Error::Config(format!("the waveform must be lora, vangelista or fsk, got {s:?}"))),
110 }
111 }
112}
113
114/// A chirp spread-spectrum link (see the module): a configuration with public fields,
115/// built by [`Css::lora`] and changed by struct update, checked by [`Css::check`] when it
116/// is used.
117#[derive(Clone, Debug, PartialEq)]
118pub struct Css {
119 /// Spreading factor: `M = 2^SF` chips per symbol.
120 pub sf: u32,
121 /// Samples per chip (1: the chip rate `B`).
122 pub oversampling: usize,
123 /// The family of waveforms.
124 pub waveform: Waveform,
125 /// The sample rate of the signal of the verb traits, `J` times the chip rate `B`
126 /// (1 Hz by default: time in samples).
127 pub sample_rate: Hertz,
128}
129
130impl Css {
131 /// The LoRa waveform at one sample per chip, at 1 Hz.
132 ///
133 /// # Errors
134 /// As [`Css::check`]: the spreading factor lies in 1..=16.
135 ///
136 /// # Examples
137 /// ```
138 /// use comnum_waveform::css::Css;
139 /// let css = Css { oversampling: 4, ..Css::lora(7)? };
140 /// assert_eq!(css.symbol_len(), 4 * 128);
141 /// assert!(Css::lora(17).is_err());
142 /// # Ok::<(), comnum_core::Error>(())
143 /// ```
144 pub fn lora(sf: u32) -> Result<Css> {
145 let css = Css { sf, oversampling: 1, waveform: Waveform::LoRa, sample_rate: Hertz(1.0) };
146 css.check()?;
147 Ok(css)
148 }
149
150 /// The number of symbols (and of chips per symbol) `M = 2^SF`.
151 pub fn m(&self) -> usize {
152 1 << self.sf
153 }
154
155 /// Samples per symbol, `J M`.
156 pub fn symbol_len(&self) -> usize {
157 self.oversampling * self.m()
158 }
159
160 /// Check the configuration.
161 ///
162 /// # Errors
163 /// [`comnum_core::Error::Config`] unless the spreading factor lies in 1..=16, the
164 /// oversampling factor is at least 1 (exactly 1 for the FSK reference) and the sample
165 /// rate is positive and finite.
166 pub fn check(&self) -> Result<()> {
167 ensure((1..=16).contains(&self.sf), || format!("the spreading factor must lie in 1..=16, got {}", self.sf))?;
168 check_rate(self.sample_rate)?;
169 ensure(self.oversampling >= 1, || "the oversampling factor must be at least 1".into())?;
170 ensure(self.waveform == Waveform::LoRa || self.oversampling == 1, || {
171 format!("the {:?} waveform is defined at one sample per chip", self.waveform)
172 })
173 }
174
175 /// The phase (rad) of symbol `a` at the time `t` counted in chips, `0 ≤ t < M`.
176 fn phase(&self, a: usize, t: f64) -> f64 {
177 let m = self.m() as f64;
178 let a = a as f64;
179 match self.waveform {
180 Waveform::LoRa => {
181 let fold = if t >= m - a { 1.0 } else { 0.0 };
182 TAU * t * (a / m - 0.5 + t / (2.0 * m) - fold)
183 }
184 // e^{j2π((a + n) mod M) n/M} = e^{j2π(n² + a n)/M} at the integer n
185 Waveform::Vangelista => TAU * (t * t + a * t) / m,
186 Waveform::Fsk => TAU * a * t / m,
187 }
188 }
189
190 /// The `J M` samples of symbol `a`.
191 ///
192 /// # Errors
193 /// [`comnum_core::Error::Config`] for invalid parameters or `a ≥ M`.
194 pub fn symbol(&self, a: usize) -> Result<Vec<C64>> {
195 self.modulate(&[a])
196 }
197
198 /// The samples of a sequence of symbols (the chirps tabulated once, see the module).
199 ///
200 /// # Errors
201 /// [`comnum_core::Error::Config`] for invalid parameters or a symbol `≥ M`.
202 pub fn modulate(&self, symbols: &[usize]) -> Result<Vec<C64>> {
203 self.check()?;
204 let m = self.m();
205 if let Some(a) = symbols.iter().find(|&&a| a >= m) {
206 return Err(comnum_core::Error::Config(format!("symbol {a} does not lie below M = {m}")));
207 }
208 let chirps = Chirps::new(self);
209 let mut out = Vec::with_capacity(symbols.len() * self.symbol_len());
210 for &a in symbols {
211 chirps.extend_symbol(&mut out, a);
212 }
213 Ok(out)
214 }
215
216 /// The energy of the `M` candidate symbols of every received symbol: the squared
217 /// magnitudes of the DFT of the dechirped samples, bins `a` and, when oversampled,
218 /// `a - M` (after the fold) added.
219 ///
220 /// # Errors
221 /// [`comnum_core::Error::Config`] for invalid parameters, [`comnum_core::Error::Shape`] for
222 /// a number of samples that is not a multiple of `J M`.
223 pub fn energies(&self, r: &[C64]) -> Result<Vec<f64>> {
224 self.check()?;
225 let (m, l) = (self.m(), self.symbol_len());
226 ensure_shape(r.len() % l == 0, || format!("{} samples is not a multiple of J M = {l}", r.len()))?;
227 let down: Vec<C64> = Chirps::new(self).base.iter().map(C64::conj).collect();
228 let mut out = Vec::with_capacity(r.len() / l * m);
229 for block in r.chunks(l) {
230 let y = fft::fft(&block.iter().zip(&down).map(|(a, b)| a * b).collect::<Vec<_>>());
231 if self.oversampling == 1 {
232 out.extend(y.iter().map(C64::norm_sqr));
233 } else {
234 out.extend((0..m).map(|k| y[k].norm_sqr() + y[(k + l - m) % l].norm_sqr()));
235 }
236 }
237 Ok(out)
238 }
239
240 /// The noncoherent decisions: the candidate of largest energy in every symbol.
241 ///
242 /// # Errors
243 /// As [`Css::energies`].
244 pub fn demodulate(&self, r: &[C64]) -> Result<Vec<usize>> {
245 let m = self.m();
246 Ok(self.energies(r)?.chunks(m).map(|e| comnum_math::argmax(e.iter().copied()).map_or(0, |(k, _)| k)).collect())
247 }
248
249 /// One symbol of an interferer of the same spreading factor, not synchronised: the
250 /// end of its symbol `s1` then the start of its symbol `s2`, which begins `tau` chips
251 /// after the symbol of interest, `0 ≤ tau < M` (Afisiadis et al. 2020, eq. (21)), at
252 /// one sample per chip.
253 ///
254 /// # Errors
255 /// [`comnum_core::Error::Config`] for another waveform than LoRa at one sample per chip,
256 /// symbols `≥ M` or `tau` outside `[0, M)`.
257 pub fn interferer(&self, s1: usize, s2: usize, tau: f64) -> Result<Vec<C64>> {
258 self.check()?;
259 let m = self.m();
260 ensure(self.waveform == Waveform::LoRa && self.oversampling == 1, || {
261 "the interferer is a LoRa signal at one sample per chip".into()
262 })?;
263 ensure(s1 < m && s2 < m, || format!("the symbols must lie below M = {m}"))?;
264 ensure((0.0..m as f64).contains(&tau), || format!("the offset must lie in [0, {m}), got {tau}"))?;
265 let mf = m as f64;
266 let x = |t: f64, s: usize| cis(TAU * (t * t / (2.0 * mf) + t * (s as f64 / mf - 0.5)));
267 Ok((0..m)
268 .map(|n| {
269 let n = n as f64;
270 if n < tau.ceil() { x(n + mf - tau, s1) } else { x(n - tau, s2) }
271 })
272 .collect())
273 }
274}
275
276/// CSS as a [`Modulate`] block: one stream of symbol indices into one stream of samples
277/// at [`Css::sample_rate`], by [`Css::modulate`].
278impl Modulate<Labels> for Css {
279 /// # Errors
280 /// [`comnum_core::Error::Config`] for several streams, and the errors of
281 /// [`Css::modulate`].
282 fn modulate_into(&self, x: &Labels, out: &mut Signal) -> Result<()> {
283 ensure(x.n_streams() == 1, || format!("CSS modulates one stream, not {}", x.n_streams()))?;
284 out.set_stream(&Css::modulate(self, x.stream(0))?, self.sample_rate);
285 Ok(())
286 }
287}
288
289/// CSS as a [`Demodulate`] block: the noncoherent decisions of [`Css::demodulate`] on one
290/// stream of samples at [`Css::sample_rate`].
291impl Demodulate<Labels> for Css {
292 /// # Errors
293 /// [`comnum_core::Error::Config`] for several streams or a signal at another sample rate,
294 /// and the errors of [`Css::demodulate`].
295 fn demodulate_into(&mut self, y: &Signal, out: &mut Labels) -> Result<()> {
296 check_input(y, self.sample_rate, "CSS")?;
297 out.set_stream(&Css::demodulate(self, y.stream(0))?);
298 Ok(())
299 }
300}
301
302/// The tables of [`Css::modulate`]: the chirp of symbol 0 and the tones
303/// $e^{j 2\pi k/(JM)}$, $k < JM$.
304struct Chirps {
305 /// The `J M` samples of symbol 0, $e^{j\varphi(0, n/J)}$.
306 base: Vec<C64>,
307 /// $e^{j 2\pi k/(JM)}$.
308 tone: Vec<C64>,
309 m: usize,
310 j: usize,
311 waveform: Waveform,
312}
313
314impl Chirps {
315 /// The tables of a checked configuration.
316 fn new(css: &Css) -> Chirps {
317 let (m, j, l) = (css.m(), css.oversampling, css.symbol_len());
318 let jf = j as f64;
319 let base = (0..l).map(|n| cis(css.phase(0, n as f64 / jf))).collect();
320 let tone = (0..l).map(|k| cis(TAU * k as f64 / l as f64)).collect();
321 Chirps { base, tone, m, j, waveform: css.waveform }
322 }
323
324 /// Append the samples of symbol `a < M`: $x_{a}\[n\] = x_{0}\[n\]\, e^{j 2\pi a n/(JM)}$,
325 /// times $e^{-j 2\pi n / J}$ after the fold of LoRa ($n \ge J(M - a)$); the FSK
326 /// has the base 1.
327 fn extend_symbol(&self, out: &mut Vec<C64>, a: usize) {
328 let l = self.base.len();
329 let fold = self.j * (self.m - a);
330 out.extend(self.base.iter().enumerate().map(|(n, b)| {
331 let v = b * self.tone[(a * n) % l];
332 if self.waveform == Waveform::LoRa && n >= fold { v * self.tone[(self.m * n) % l].conj() } else { v }
333 }));
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 /// The verb traits give the samples and the decisions of the slice methods, bit for
342 /// bit, at the sample rate of the configuration; an input at another rate or with two
343 /// streams is refused.
344 #[test]
345 fn the_verb_traits_are_the_slice_methods() {
346 let mut rng = comnum_core::rng::Rng::new(7);
347 for (sf, oversampling, waveform) in
348 [(7u32, 1usize, Waveform::LoRa), (8, 4, Waveform::LoRa), (6, 1, Waveform::Fsk)]
349 {
350 let mut css = Css { sf, oversampling, waveform, sample_rate: Hertz(250e3) };
351 let a: Vec<usize> = (0..50).map(|_| rng.below(css.m())).collect();
352 let s = css.modulate(&a).unwrap();
353 let mut x = Signal::zeros(0, 0, Hertz(1.0));
354 css.modulate_into(&Labels::from_stream(a.clone()).unwrap(), &mut x).unwrap();
355 assert_eq!((x.stream(0), x.sample_rate()), (&s[..], Hertz(250e3)));
356 // a noisy signal: the decisions of the trait are those of the method
357 let noisy: Vec<C64> = s.iter().map(|v| v + C64::new(rng.gaussian(), rng.gaussian()) * 3.0).collect();
358 let y = Signal::from_stream(noisy.clone(), Hertz(250e3)).unwrap();
359 let mut out = Labels::default();
360 css.demodulate_into(&y, &mut out).unwrap();
361 assert_eq!(out.stream(0), css.demodulate(&noisy).unwrap());
362 assert!(css.demodulate_into(&Signal::from_stream(noisy, Hertz(1.0)).unwrap(), &mut out).is_err());
363 assert!(Modulate::modulate(&css, &Labels::zeros(2, 4)).is_err());
364 }
365 assert!(Css { sample_rate: Hertz(0.0), ..Css::lora(7).unwrap() }.check().is_err());
366 }
367
368 /// The chirps were computed from their phase, sample after sample and symbol after
369 /// symbol, before they were tabulated: the same samples within $10^{-11}$ up to
370 /// SF 12, symbol 0 exactly. Against the phase reduced exactly modulo
371 /// $2\pi$ (an integer numerator over $2M$, LoRa at one sample per chip), the
372 /// tabulated chirps are the closer: the direct evaluation carries the rounding of a
373 /// phase of up to $\pi M$ rad ($4.2 \cdot 10^{-12}$ at SF 12, $6.3 \cdot 10^{-13}$
374 /// tabulated).
375 #[test]
376 fn the_tabulated_chirps_are_the_direct_ones() {
377 for (sf, oversampling, waveform) in [
378 (7u32, 1usize, Waveform::LoRa),
379 (7, 4, Waveform::LoRa),
380 (12, 1, Waveform::LoRa),
381 (9, 8, Waveform::LoRa),
382 (7, 1, Waveform::Vangelista),
383 (10, 1, Waveform::Vangelista),
384 (7, 1, Waveform::Fsk),
385 ] {
386 let css = Css { sf, oversampling, waveform, sample_rate: Hertz(1.0) };
387 let m = css.m();
388 let jf = oversampling as f64;
389 let symbols: Vec<usize> = (0..m).step_by((m / 64).max(1)).chain([m - 1]).collect();
390 let fast = css.modulate(&symbols).unwrap();
391 for (i, &a) in symbols.iter().enumerate() {
392 let direct: Vec<C64> = (0..css.symbol_len()).map(|n| cis(css.phase(a, n as f64 / jf))).collect();
393 let got = &fast[i * css.symbol_len()..(i + 1) * css.symbol_len()];
394 let err = got.iter().zip(&direct).map(|(x, y)| (x - y).norm()).fold(0.0, f64::max);
395 assert!(err < 1e-11, "SF {sf} J {oversampling} {waveform:?} symbol {a}: {err}");
396 assert!(a != 0 || err == 0.0, "{waveform:?} symbol 0: {err}");
397 }
398 }
399 assert!(Css::lora(7).unwrap().modulate(&[3, 128]).is_err());
400 for sf in [7u32, 10, 12] {
401 let css = Css::lora(sf).unwrap();
402 let m = css.m();
403 let (mut direct, mut tabulated) = (0.0f64, 0.0f64);
404 for a in (0..m).step_by(m / 32) {
405 let fast = css.symbol(a).unwrap();
406 for (n, v) in fast.iter().enumerate() {
407 // 2π (n² + (2a - M) n - 2 M n [n ≥ M - a]) / (2M), the numerator reduced
408 let fold = usize::from(n >= m - a);
409 let num = (n * n + 2 * a * n + 4 * m * n - m * n - 2 * m * n * fold) % (2 * m);
410 let exact = cis(TAU * num as f64 / (2 * m) as f64);
411 direct = direct.max((cis(css.phase(a, n as f64)) - exact).norm());
412 tabulated = tabulated.max((v - exact).norm());
413 }
414 }
415 assert!(tabulated < direct, "SF {sf}: tabulated {tabulated:e}, direct {direct:e}");
416 }
417 }
418
419 #[test]
420 fn the_waveforms_are_orthogonal_and_the_fft_receiver_is_the_correlator_bank() {
421 // Vangelista 2017, eqs. (9) and (16); Chiani and Elzanaty 2019, Property 3
422 for waveform in [Waveform::LoRa, Waveform::Vangelista, Waveform::Fsk] {
423 let css = Css { sf: 5, oversampling: 1, waveform, sample_rate: Hertz(1.0) };
424 let m = css.m();
425 let x: Vec<Vec<C64>> = (0..m).map(|a| css.symbol(a).unwrap()).collect();
426 for i in 0..m {
427 for q in 0..m {
428 let c: C64 = x[i].iter().zip(&x[q]).map(|(a, b)| a * b.conj()).sum::<C64>() / m as f64;
429 let want = if i == q { 1.0 } else { 0.0 };
430 assert!((c - want).norm() < 1e-12, "{waveform:?}: <{i}, {q}> = {c}");
431 }
432 }
433 // a received block: the DFT receiver gives the energies of the correlators
434 let r: Vec<C64> = (0..m).map(|n| C64::new((0.3 * n as f64).sin(), (1.7 * n as f64).cos())).collect();
435 let e = css.energies(&r).unwrap();
436 for (q, xq) in x.iter().enumerate() {
437 let c: C64 = r.iter().zip(xq).map(|(a, b)| a * b.conj()).sum();
438 assert!((c.norm_sqr() - e[q]).abs() < 1e-9, "{waveform:?}: bin {q}");
439 }
440 }
441 }
442
443 #[test]
444 fn lora_is_a_continuous_phase_chirp_sampled_at_any_rate() {
445 // Chiani and Elzanaty 2019, Property 1: the phase starts and ends at 0 (mod 2π)
446 let css = Css { sf: 6, oversampling: 8, waveform: Waveform::LoRa, sample_rate: Hertz(1.0) };
447 let m = css.m() as f64;
448 for a in [0usize, 1, 17, 63] {
449 let end = css.phase(a, m - 1e-9).rem_euclid(TAU);
450 assert!(end.min(TAU - end) < 1e-6, "symbol {a}: {end}");
451 // the instantaneous frequency rises by B/M per chip and folds once
452 let x = css.symbol(a).unwrap();
453 let df: Vec<f64> = x.windows(2).map(|w| (w[1] * w[0].conj()).arg() * 8.0 / TAU).collect();
454 assert!((df[0] - (a as f64 / m - 0.5)).abs() < 0.02, "{a}: {}", df[0]);
455 }
456 // oversampled, the receiver still decides right without noise
457 let s = css.modulate(&[3, 0, 63, 40]).unwrap();
458 assert_eq!(css.demodulate(&s).unwrap(), vec![3, 0, 63, 40]);
459 // at one sample per chip it is eq. (13)
460 let one = Css::lora(6).unwrap();
461 let x = one.symbol(9).unwrap();
462 for (n, v) in x.iter().enumerate() {
463 let n = n as f64;
464 let want = cis(TAU * n * (9.0 / 64.0 - 0.5 + n / 128.0));
465 assert!((v - want).norm() < 1e-9);
466 }
467 assert!(Css { sf: 6, oversampling: 2, waveform: Waveform::Fsk, sample_rate: Hertz(1.0) }.symbol(0).is_err());
468 assert!(one.symbol(64).is_err() && Css::lora(0).is_err());
469 }
470
471 #[test]
472 fn the_interferer_is_the_lora_signal_shifted_by_an_integer_offset() {
473 // for an integer τ, eq. (21) is the stream of the two symbols delayed by τ chips
474 let css = Css::lora(5).unwrap();
475 let stream = css.modulate(&[7, 20]).unwrap();
476 for tau in [0usize, 1, 13, 31] {
477 let y = css.interferer(7, 20, tau as f64).unwrap();
478 for (n, v) in y.iter().enumerate() {
479 assert!((v - stream[n + 32 - tau]).norm() < 1e-9, "τ = {tau}, n = {n}");
480 }
481 }
482 assert!(css.interferer(0, 0, 32.0).is_err());
483 }
484}