1use crate::{check_input, check_rate};
90use comnum_core::signal::{Signal, Symbols};
91use comnum_core::traits::{Demodulate, Modulate};
92use comnum_core::units::Hertz;
93use comnum_core::{Error, Result, ensure, ensure_shape};
94use comnum_dsp::filter::normalize_energy;
95use comnum_math::{C64, cis, fft};
96
97fn oqam_phase(q: usize) -> C64 {
99 cis(std::f64::consts::FRAC_PI_2 * (q % 4) as f64)
100}
101
102#[non_exhaustive]
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum Prototype {
106 Phydyas,
108 Hermite,
111}
112
113const HERMITE_COEFFICIENTS: [f64; 6] = [1.412_692_577, -3.0145e-3, -8.8041e-6, -2.2611e-9, -4.4570e-15, 1.8633e-16];
116
117impl Prototype {
118 pub fn taps(self, subcarriers: usize, overlap: usize) -> Result<Vec<f64>> {
124 if self == Prototype::Hermite {
125 ensure(overlap >= 1, || "the Hermite pulse needs an overlapping factor of at least 1".into())?;
126 let t0 = subcarriers as f64;
127 let centre = (overlap * subcarriers) as f64 / 2.0 - 1.0;
128 let mut h: Vec<f64> = (0..overlap * subcarriers - 1)
129 .map(|k| {
130 let t = (k as f64 - centre) / t0;
131 let x = 2.0 * std::f64::consts::PI.sqrt() * t;
132 let poly: f64 = HERMITE_COEFFICIENTS
133 .iter()
134 .enumerate()
135 .map(|(i, a)| a * comnum_math::hermite_h(4 * i, x))
136 .sum();
137 (-std::f64::consts::TAU * t * t).exp() * poly
138 })
139 .collect();
140 normalize_energy(&mut h);
141 return Ok(h);
142 }
143 let freq: &[f64] = match (self, overlap) {
144 (Prototype::Phydyas, 2) => &[1.0, std::f64::consts::FRAC_1_SQRT_2],
145 (Prototype::Phydyas, 3) => &[1.0, 0.911_437_83, 0.411_437_83],
146 (Prototype::Phydyas, 4) => &[1.0, 0.971_959_83, std::f64::consts::FRAC_1_SQRT_2, 0.235_146_95],
147 _ => {
148 return Err(Error::Config(format!(
149 "no PHYDYAS filter for an overlapping factor of {overlap}: 2, 3 or 4"
150 )));
151 }
152 };
153 let km = (overlap * subcarriers) as f64;
154 let mut h: Vec<f64> = (0..overlap * subcarriers - 1)
155 .map(|k| {
156 let tail: f64 = freq
157 .iter()
158 .enumerate()
159 .skip(1)
160 .map(|(i, hi)| {
161 let sign = if i % 2 == 1 { -1.0 } else { 1.0 };
162 2.0 * sign * hi * (std::f64::consts::TAU * i as f64 * (k + 1) as f64 / km).cos()
163 })
164 .sum();
165 freq[0] + tail
166 })
167 .collect();
168 normalize_energy(&mut h);
169 Ok(h)
170 }
171
172 pub fn localisation(self, subcarriers: usize, overlap: usize) -> Result<(f64, f64)> {
182 let h = self.taps(subcarriers, overlap)?;
183 let t0 = subcarriers as f64;
184 let energy: f64 = h.iter().map(|v| v * v).sum();
185 let mean_t: f64 = h.iter().enumerate().map(|(k, v)| k as f64 * v * v).sum::<f64>() / energy;
186 let var_t: f64 = h.iter().enumerate().map(|(k, v)| (k as f64 - mean_t).powi(2) * v * v).sum::<f64>() / energy;
187 let n = (16 * h.len()).next_power_of_two();
188 let mut x = vec![C64::new(0.0, 0.0); n];
189 for (xk, hk) in x.iter_mut().zip(&h) {
190 *xk = C64::new(*hk, 0.0);
191 }
192 let spectrum = fft::fft(&x);
193 let (mut e, mut m1, mut m2) = (0.0, 0.0, 0.0);
195 for (k, v) in spectrum.iter().enumerate() {
196 let f = if k < n / 2 { k as f64 } else { k as f64 - n as f64 } / n as f64;
197 let p = v.norm_sqr();
198 e += p;
199 m1 += f * p;
200 m2 += f * f * p;
201 }
202 let (m1, m2) = (m1 / e, m2 / e);
203 Ok((var_t.sqrt() / t0, (m2 - m1 * m1).sqrt() * t0))
204 }
205}
206
207#[derive(Clone, Debug, PartialEq)]
209pub struct Fbmc {
210 n_fft: usize,
211 overlap: usize,
212 taps: Vec<f64>,
213 sample_rate: Hertz,
214}
215
216impl Fbmc {
217 pub fn new(n_fft: usize, overlap: usize) -> Result<Fbmc> {
225 Fbmc::with_prototype(n_fft, overlap, Prototype::Phydyas)
226 }
227
228 pub fn with_prototype(n_fft: usize, overlap: usize, prototype: Prototype) -> Result<Fbmc> {
233 ensure(n_fft >= 2 && n_fft % 2 == 0, || format!("FBMC/OQAM needs an even number of subcarriers, got {n_fft}"))?;
234 Ok(Fbmc { n_fft, overlap, taps: prototype.taps(n_fft, overlap)?, sample_rate: Hertz(1.0) })
235 }
236
237 pub fn with_sample_rate(self, sample_rate: Hertz) -> Result<Fbmc> {
252 check_rate(sample_rate)?;
253 Ok(Fbmc { sample_rate, ..self })
254 }
255
256 pub fn sample_rate(&self) -> Hertz {
258 self.sample_rate
259 }
260
261 pub fn n_fft(&self) -> usize {
263 self.n_fft
264 }
265
266 pub fn overlap(&self) -> usize {
268 self.overlap
269 }
270
271 pub fn taps(&self) -> &[f64] {
273 &self.taps
274 }
275
276 pub fn n_samples(&self, half_symbols: usize) -> usize {
279 if half_symbols == 0 { 0 } else { (half_symbols - 1) * self.n_fft / 2 + self.taps.len() }
280 }
281
282 pub fn half_symbols(&self, n_samples: usize) -> Result<usize> {
288 let (half, l) = (self.n_fft / 2, self.taps.len());
289 if n_samples == 0 {
290 return Ok(0);
291 }
292 ensure_shape(n_samples >= l && (n_samples - l) % half == 0, || {
293 format!("{n_samples} samples are not a whole number of half-symbols ((n - 1) {half} + {l})")
294 })?;
295 Ok((n_samples - l) / half + 1)
296 }
297
298 pub fn pulse(&self, m: usize, n: usize) -> (usize, Vec<C64>) {
300 let mm = self.n_fft as f64;
301 let centre = (self.taps.len() - 1) as f64 / 2.0;
302 let phase = oqam_phase(m + n);
303 let values = self
304 .taps
305 .iter()
306 .enumerate()
307 .map(|(i, h)| cis(std::f64::consts::TAU * m as f64 * (i as f64 - centre) / mm) * phase * *h)
308 .collect();
309 (n * self.n_fft / 2, values)
310 }
311
312 pub fn interference(&self, m: usize, n: usize, dm: isize, dn: isize) -> C64 {
319 if dn.unsigned_abs() >= 2 * self.overlap {
320 return C64::new(0.0, 0.0);
321 }
322 let mm = self.n_fft as isize;
323 let m2 = (m as isize + dm).rem_euclid(mm) as usize;
324 let base = 4 * self.overlap;
326 let (n1, n2) = (n + base, (n as isize + base as isize + dn) as usize);
327 let (s1, g1) = self.pulse(m, n1);
328 let (s2, g2) = self.pulse(m2, n2);
329 let (start, end) = (s1.max(s2), (s1 + g1.len()).min(s2 + g2.len()));
330 (start..end).map(|k| g2[k - s2] * g1[k - s1].conj()).sum()
333 }
334
335 pub fn modulate(&self, a: &[f64]) -> Result<Vec<C64>> {
341 let m = self.n_fft;
342 ensure_shape(a.len() % m == 0, || format!("{} real symbols for {m} subcarriers", a.len()))?;
343 let half_symbols = a.len() / m;
344 let shift = (self.taps.len() - 1) / 2;
345 let plan = fft::plan(m);
346 let mut s = vec![C64::new(0.0, 0.0); self.n_samples(half_symbols)];
347 let mut v = vec![C64::new(0.0, 0.0); m];
348 for (n, row) in a.chunks_exact(m).enumerate() {
349 for (k, (vk, ak)) in v.iter_mut().zip(row).enumerate() {
350 *vk = oqam_phase(k + n) * *ak;
351 }
352 plan.inverse(&mut v);
354 let offset = n * m / 2;
355 for (i, h) in self.taps.iter().enumerate() {
356 s[offset + i] += v[(i + m * self.overlap - shift) % m] * (h * m as f64);
357 }
358 }
359 Ok(s)
360 }
361
362 pub fn demodulate(&self, r: &[C64]) -> Result<Vec<C64>> {
369 let m = self.n_fft;
370 let half_symbols = self.half_symbols(r.len())?;
371 let shift = (self.taps.len() - 1) / 2;
372 let plan = fft::plan(m);
373 let mut y = Vec::with_capacity(half_symbols * m);
374 let mut w = vec![C64::new(0.0, 0.0); m];
375 for n in 0..half_symbols {
376 w.fill(C64::new(0.0, 0.0));
377 let offset = n * m / 2;
378 for (i, h) in self.taps.iter().enumerate() {
379 w[(i + m * self.overlap - shift) % m] += r[offset + i] * *h;
380 }
381 plan.forward(&mut w);
382 for (k, wk) in w.iter().enumerate() {
383 y.push(wk * oqam_phase(k + n).conj());
384 }
385 }
386 Ok(y)
387 }
388}
389
390impl Modulate for Fbmc {
394 fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
395 let a = oqam_stagger(crate::one_stream(x, "FBMC")?, self.n_fft)?;
396 out.set_stream(&Fbmc::modulate(self, &a)?, self.sample_rate);
397 Ok(())
398 }
399}
400
401impl Demodulate for Fbmc {
404 fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
409 check_input(y, self.sample_rate, "FBMC")?;
410 let re: Vec<f64> = Fbmc::demodulate(self, y.stream(0))?.iter().map(|v| v.re).collect();
411 out.set_stream(&oqam_destagger(&re, self.n_fft)?);
412 Ok(())
413 }
414}
415
416pub fn oqam_stagger(c: &[C64], m: usize) -> Result<Vec<f64>> {
424 ensure_shape(m > 0 && c.len() % m == 0, || {
425 format!("{} symbols are not whole symbols of {m} subcarriers", c.len())
426 })?;
427 let mut a = vec![0.0; 2 * c.len()];
428 for (k, row) in c.chunks_exact(m).enumerate() {
429 for (sub, v) in row.iter().enumerate() {
430 let (first, second) = if sub % 2 == 0 { (v.re, v.im) } else { (v.im, v.re) };
431 a[2 * k * m + sub] = first;
432 a[(2 * k + 1) * m + sub] = second;
433 }
434 }
435 Ok(a)
436}
437
438pub fn oqam_destagger(a: &[f64], m: usize) -> Result<Vec<C64>> {
445 ensure_shape(m > 0 && a.len() % (2 * m) == 0, || {
446 format!("{} real symbols are not an even number of half-symbols of {m} subcarriers", a.len())
447 })?;
448 let mut c = Vec::with_capacity(a.len() / 2);
449 for pair in a.chunks_exact(2 * m) {
450 let (first, second) = pair.split_at(m);
451 for sub in 0..m {
452 c.push(if sub % 2 == 0 { C64::new(first[sub], second[sub]) } else { C64::new(second[sub], first[sub]) });
453 }
454 }
455 Ok(c)
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461 use comnum_core::rng::Rng;
462
463 #[test]
467 fn the_hermite_pulse_is_orthogonal_at_tf_2_and_on_the_oqam_lattice() {
468 let f = Fbmc::with_prototype(64, 8, Prototype::Hermite).unwrap();
469 assert_eq!(f.taps().len(), 8 * 64 - 1);
470 for dm in -6isize..=6 {
471 for dn in -16isize..=16 {
472 if dm == 0 && dn == 0 {
473 continue;
474 }
475 let v = f.interference(8, 16, dm, dn);
476 assert!(v.re.abs() < 1e-6, "real part {v} at ({dm}, {dn})");
477 if dm % 2 == 0 && dn % 2 == 0 {
478 assert!(v.norm() < 1e-6, "complex {v} at ({dm}, {dn})");
479 }
480 }
481 }
482 assert!(f.interference(8, 16, 0, 1).im.abs() > 0.1);
484 assert!(Prototype::Hermite.taps(16, 0).is_err());
485 }
486
487 #[test]
491 fn the_localisation_of_the_prototypes_is_that_of_nissel_2017() {
492 for m in [32, 256] {
493 let (st, sf) = Prototype::Hermite.localisation(m, 8).unwrap();
494 assert!((st - 0.2015).abs() < 5e-4 && (sf - 0.403).abs() < 5e-4, "Hermite {st} {sf}");
495 assert!((st * sf * 4.0 * std::f64::consts::PI - 1.02).abs() < 5e-3);
496 let (st, sf) = Prototype::Phydyas.localisation(m, 4).unwrap();
497 assert!((st - 0.2745).abs() < 5e-4 && (sf - 0.328).abs() < 5e-4, "PHYDYAS {st} {sf}");
498 assert!((st * sf * 4.0 * std::f64::consts::PI - 1.13).abs() < 5e-3);
499 }
500 }
501
502 #[test]
506 fn the_shared_phase_and_normalisation_are_the_former_ones() {
507 let f = Fbmc::new(16, 4).unwrap();
508 let mut r = Rng::new(6);
509 let a: Vec<f64> = (0..16 * 10).map(|_| r.gaussian()).collect();
510 let (m, shift) = (16usize, (f.taps().len() - 1) / 2);
511 let plan = fft::plan(m);
512 let mut s = vec![C64::new(0.0, 0.0); f.n_samples(10)];
513 for (n, row) in a.chunks_exact(m).enumerate() {
514 let mut v: Vec<C64> = row
515 .iter()
516 .enumerate()
517 .map(|(k, ak)| cis(std::f64::consts::FRAC_PI_2 * ((k + n) % 4) as f64) * *ak)
518 .collect();
519 plan.inverse(&mut v);
520 for (i, h) in f.taps().iter().enumerate() {
521 s[n * m / 2 + i] += v[(i + m * 4 - shift) % m] * (h * m as f64);
522 }
523 }
524 assert_eq!(f.modulate(&a).unwrap(), s);
525 let y = f.demodulate(&s).unwrap();
526 for n in 0..10 {
527 let mut w = vec![C64::new(0.0, 0.0); m];
528 for (i, h) in f.taps().iter().enumerate() {
529 w[(i + m * 4 - shift) % m] += s[n * m / 2 + i] * *h;
530 }
531 plan.forward(&mut w);
532 for (k, wk) in w.iter().enumerate() {
533 assert_eq!(y[n * m + k], wk * cis(-std::f64::consts::FRAC_PI_2 * ((k + n) % 4) as f64));
534 }
535 }
536 let raw = Prototype::Phydyas.taps(16, 4).unwrap();
537 let norm = raw.iter().map(|v| v * v).sum::<f64>().sqrt();
538 assert!((norm - 1.0).abs() < 1e-15);
539 }
540
541 #[test]
542 fn the_traits_stagger_inside() {
543 let f = Fbmc::new(16, 4).unwrap().with_sample_rate(Hertz(1e6)).unwrap();
544 let c: Vec<C64> = (0..16 * 6)
545 .map(|i| C64::new(if i % 3 == 0 { 1.0 } else { -1.0 }, if i % 5 == 0 { -1.0 } else { 1.0 }))
546 .collect();
547 let x = Modulate::modulate(&f, &Symbols::from_stream(c.clone()).unwrap()).unwrap();
548 assert_eq!((x.len(), x.sample_rate()), (f.n_samples(12), Hertz(1e6)));
549 assert_eq!(x.stream(0), &f.modulate(&oqam_stagger(&c, 16).unwrap()).unwrap()[..]);
550 let mut g = f.clone();
551 let back = Demodulate::demodulate(&mut g, &x).unwrap();
552 assert!(back.as_slice().iter().zip(&c).all(|(a, b)| (a - b).norm() < 1e-2));
553 let other = Signal::from_stream(x.stream(0).to_vec(), Hertz(2e6)).unwrap();
554 assert!(Demodulate::demodulate(&mut g, &other).is_err() && f.clone().with_sample_rate(Hertz(-1.0)).is_err());
555 }
556
557 #[test]
558 fn distant_pulses_do_not_interfere() {
559 let f = Fbmc::new(8, 4).unwrap();
560 assert_eq!(f.interference(0, 0, 1, -1000), C64::new(0.0, 0.0));
561 assert_eq!(f.interference(0, 0, 1, isize::MAX), C64::new(0.0, 0.0));
562 assert!(f.interference(0, 0, 1, 7).norm() > 0.0);
563 }
564
565 #[test]
566 fn the_pulses_are_real_orthogonal_and_of_unit_energy() {
567 for (m, k) in [(8usize, 2usize), (16, 3), (32, 4)] {
568 let f = Fbmc::new(m, k).unwrap();
569 assert!((f.taps().iter().map(|v| v * v).sum::<f64>() - 1.0).abs() < 1e-12);
570 for m0 in [0usize, 1, 5] {
571 for n0 in [0usize, 1, 2, 3] {
572 for dm in -2isize..=2 {
573 for dn in -(2 * k as isize)..=(2 * k as isize) {
574 let v = f.interference(m0, n0, dm, dn);
575 let want = if dm == 0 && dn == 0 { 1.0 } else { 0.0 };
576 let tol = match k {
579 2 => 0.05,
580 3 => 5e-3,
581 _ => 5e-4,
582 };
583 assert!((v.re - want).abs() < tol, "M {m} K {k} ({m0},{n0}) + ({dm},{dn}): {v}");
584 }
585 }
586 }
587 }
588 }
589 }
590
591 #[test]
592 fn modulation_is_the_sum_of_the_pulses_and_demodulation_their_projection() {
593 let f = Fbmc::new(8, 4).unwrap();
594 let mut r = Rng::new(3);
595 let n = 5;
596 let a: Vec<f64> = (0..8 * n).map(|_| r.gaussian()).collect();
597 let s = f.modulate(&a).unwrap();
598 let mut direct = vec![C64::new(0.0, 0.0); f.n_samples(n)];
599 for nn in 0..n {
600 for m in 0..8 {
601 let (start, g) = f.pulse(m, nn);
602 for (i, v) in g.iter().enumerate() {
603 direct[start + i] += v * a[nn * 8 + m];
604 }
605 }
606 }
607 assert!(s.iter().zip(&direct).all(|(x, y)| (x - y).norm() < 1e-12));
608 let y = f.demodulate(&s).unwrap();
609 for nn in 0..n {
610 for m in 0..8 {
611 let (start, g) = f.pulse(m, nn);
612 let proj: C64 = g.iter().enumerate().map(|(i, v)| s[start + i] * v.conj()).sum();
613 assert!((y[nn * 8 + m] - proj).norm() < 1e-12);
614 }
615 }
616 }
617
618 #[test]
619 fn unit_power_and_half_the_noise_on_each_real_estimate() {
620 let (m, n) = (64usize, 400usize);
621 let f = Fbmc::new(m, 4).unwrap();
622 let mut r = Rng::new(9);
623 let a: Vec<f64> = (0..m * n).map(|_| if r.below(2) == 0 { 0.5f64.sqrt() } else { -0.5f64.sqrt() }).collect();
624 let s = f.modulate(&a).unwrap();
625 let mid = &s[f.taps().len()..s.len() - f.taps().len()];
627 let p = mid.iter().map(C64::norm_sqr).sum::<f64>() / mid.len() as f64;
628 assert!((p - 1.0).abs() < 0.02, "{p}");
629 let sigma2: f64 = 0.1;
630 let noisy: Vec<C64> = s.iter().map(|v| v + r.complex_gaussian() * sigma2.sqrt()).collect();
631 let y = f.demodulate(&noisy).unwrap();
632 let err: Vec<f64> = y.iter().zip(&a).map(|(v, a)| v.re - a).collect();
633 let var = err.iter().map(|e| e * e).sum::<f64>() / err.len() as f64;
634 assert!((var / (sigma2 / 2.0) - 1.0).abs() < 0.03, "{var}");
635 }
636
637 #[test]
638 fn the_spectrum_is_confined_where_that_of_ofdm_is_not() {
639 let (m, n) = (32usize, 16usize);
644 let f = Fbmc::new(m, 4).unwrap();
645 let mut a = vec![0.0; m * n];
646 let mut r = Rng::new(4);
647 let mut p = vec![0.0; 16 * m];
648 for _ in 0..50 {
649 for nn in 0..n {
650 a[nn * m] = if r.below(2) == 0 { 1.0 } else { -1.0 };
651 }
652 let mut s = f.modulate(&a).unwrap();
653 s.resize(16 * m * 8, C64::new(0.0, 0.0));
654 let spec = fft::fft(&s);
655 for (i, v) in p.iter_mut().enumerate() {
657 *v += spec[i * 8].norm_sqr();
658 }
659 }
660 let at = |k: f64| p[((k * 16.0).round() as isize).rem_euclid(16 * m as isize) as usize];
661 let fbmc_db = comnum_math::lin2db(at(8.5) / at(0.0));
662 let ofdm_db = 20.0 * comnum_math::sinc(8.5).abs().log10();
664 assert!(fbmc_db < -50.0 && ofdm_db > -30.0, "FBMC {fbmc_db} dB, OFDM {ofdm_db} dB");
665 }
666
667 #[test]
668 fn staggering_round_trips() {
669 let c: Vec<C64> = (0..12).map(|i| C64::new(i as f64, -(i as f64) - 0.5)).collect();
670 let a = oqam_stagger(&c, 4).unwrap();
671 assert_eq!(a.len(), 24);
672 assert_eq!((a[0], a[4], a[1], a[5]), (0.0, -0.5, -1.5, 1.0));
673 assert_eq!(oqam_destagger(&a, 4).unwrap(), c);
674 assert!(oqam_stagger(&c[..5], 4).is_err() && oqam_destagger(&a[..20], 4).is_err());
675 }
676}
677
678#[cfg(test)]
679mod literature {
680 use super::*;
681
682 #[test]
689 fn bouhadda_2014_table_1() {
690 let f = Fbmc::new(64, 4).unwrap();
691 let rows: [(isize, [f64; 7]); 3] = [
692 (1, [0.043, 0.125, 0.206, 0.239, 0.206, 0.125, 0.043]),
693 (0, [-0.067, 0.0, -0.564, 0.0, 0.564, 0.0, 0.067]),
694 (-1, [0.043, -0.125, 0.206, -0.239, 0.206, -0.125, 0.043]),
695 ];
696 for (dm, row) in rows {
697 for (j, want) in row.iter().enumerate() {
698 let dn = j as isize - 3;
699 let v = f.interference(10, 10, dm, dn);
700 if dm == 0 && dn == 0 {
701 assert!((v - 1.0).norm() < 1e-3);
702 } else {
703 assert!(v.re.abs() < 5e-4 && (v.im - want).abs() < 5e-4, "({dm}, {dn}): {v} against {want}j");
704 }
705 }
706 }
707 }
708}