1use crate::{check_input, check_rate};
77use comnum_core::signal::{Signal, Symbols};
78use comnum_core::traits::{Demodulate, Modulate};
79use comnum_core::units::Hertz;
80use comnum_core::{Error, Result, ensure, ensure_shape};
81use comnum_dsp::filter::normalize_energy;
82use comnum_math::{C64, cis, fft};
83
84fn oqam_phase(q: usize) -> C64 {
86 cis(std::f64::consts::FRAC_PI_2 * (q % 4) as f64)
87}
88
89#[non_exhaustive]
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum Prototype {
93 Phydyas,
95}
96
97impl Prototype {
98 pub fn taps(self, subcarriers: usize, overlap: usize) -> Result<Vec<f64>> {
104 let freq: &[f64] = match (self, overlap) {
105 (Prototype::Phydyas, 2) => &[1.0, std::f64::consts::FRAC_1_SQRT_2],
106 (Prototype::Phydyas, 3) => &[1.0, 0.911_437_83, 0.411_437_83],
107 (Prototype::Phydyas, 4) => &[1.0, 0.971_959_83, std::f64::consts::FRAC_1_SQRT_2, 0.235_146_95],
108 _ => {
109 return Err(Error::Config(format!(
110 "no PHYDYAS filter for an overlapping factor of {overlap}: 2, 3 or 4"
111 )));
112 }
113 };
114 let km = (overlap * subcarriers) as f64;
115 let mut h: Vec<f64> = (0..overlap * subcarriers - 1)
116 .map(|k| {
117 let tail: f64 = freq
118 .iter()
119 .enumerate()
120 .skip(1)
121 .map(|(i, hi)| {
122 let sign = if i % 2 == 1 { -1.0 } else { 1.0 };
123 2.0 * sign * hi * (std::f64::consts::TAU * i as f64 * (k + 1) as f64 / km).cos()
124 })
125 .sum();
126 freq[0] + tail
127 })
128 .collect();
129 normalize_energy(&mut h);
130 Ok(h)
131 }
132}
133
134#[derive(Clone, Debug, PartialEq)]
136pub struct Fbmc {
137 n_fft: usize,
138 overlap: usize,
139 taps: Vec<f64>,
140 sample_rate: Hertz,
141}
142
143impl Fbmc {
144 pub fn new(n_fft: usize, overlap: usize) -> Result<Fbmc> {
152 Fbmc::with_prototype(n_fft, overlap, Prototype::Phydyas)
153 }
154
155 pub fn with_prototype(n_fft: usize, overlap: usize, prototype: Prototype) -> Result<Fbmc> {
160 ensure(n_fft >= 2 && n_fft % 2 == 0, || format!("FBMC/OQAM needs an even number of subcarriers, got {n_fft}"))?;
161 Ok(Fbmc { n_fft, overlap, taps: prototype.taps(n_fft, overlap)?, sample_rate: Hertz(1.0) })
162 }
163
164 pub fn with_sample_rate(self, sample_rate: Hertz) -> Result<Fbmc> {
179 check_rate(sample_rate)?;
180 Ok(Fbmc { sample_rate, ..self })
181 }
182
183 pub fn sample_rate(&self) -> Hertz {
185 self.sample_rate
186 }
187
188 pub fn n_fft(&self) -> usize {
190 self.n_fft
191 }
192
193 pub fn overlap(&self) -> usize {
195 self.overlap
196 }
197
198 pub fn taps(&self) -> &[f64] {
200 &self.taps
201 }
202
203 pub fn n_samples(&self, half_symbols: usize) -> usize {
206 if half_symbols == 0 { 0 } else { (half_symbols - 1) * self.n_fft / 2 + self.taps.len() }
207 }
208
209 pub fn half_symbols(&self, n_samples: usize) -> Result<usize> {
215 let (half, l) = (self.n_fft / 2, self.taps.len());
216 if n_samples == 0 {
217 return Ok(0);
218 }
219 ensure_shape(n_samples >= l && (n_samples - l) % half == 0, || {
220 format!("{n_samples} samples are not a whole number of half-symbols ((n - 1) {half} + {l})")
221 })?;
222 Ok((n_samples - l) / half + 1)
223 }
224
225 pub fn pulse(&self, m: usize, n: usize) -> (usize, Vec<C64>) {
227 let mm = self.n_fft as f64;
228 let centre = (self.taps.len() - 1) as f64 / 2.0;
229 let phase = oqam_phase(m + n);
230 let values = self
231 .taps
232 .iter()
233 .enumerate()
234 .map(|(i, h)| cis(std::f64::consts::TAU * m as f64 * (i as f64 - centre) / mm) * phase * *h)
235 .collect();
236 (n * self.n_fft / 2, values)
237 }
238
239 pub fn interference(&self, m: usize, n: usize, dm: isize, dn: isize) -> C64 {
246 if dn.unsigned_abs() >= 2 * self.overlap {
247 return C64::new(0.0, 0.0);
248 }
249 let mm = self.n_fft as isize;
250 let m2 = (m as isize + dm).rem_euclid(mm) as usize;
251 let base = 4 * self.overlap;
253 let (n1, n2) = (n + base, (n as isize + base as isize + dn) as usize);
254 let (s1, g1) = self.pulse(m, n1);
255 let (s2, g2) = self.pulse(m2, n2);
256 let (start, end) = (s1.max(s2), (s1 + g1.len()).min(s2 + g2.len()));
257 (start..end).map(|k| g2[k - s2] * g1[k - s1].conj()).sum()
260 }
261
262 pub fn modulate(&self, a: &[f64]) -> Result<Vec<C64>> {
268 let m = self.n_fft;
269 ensure_shape(a.len() % m == 0, || format!("{} real symbols for {m} subcarriers", a.len()))?;
270 let half_symbols = a.len() / m;
271 let shift = (self.taps.len() - 1) / 2;
272 let plan = fft::plan(m);
273 let mut s = vec![C64::new(0.0, 0.0); self.n_samples(half_symbols)];
274 let mut v = vec![C64::new(0.0, 0.0); m];
275 for (n, row) in a.chunks_exact(m).enumerate() {
276 for (k, (vk, ak)) in v.iter_mut().zip(row).enumerate() {
277 *vk = oqam_phase(k + n) * *ak;
278 }
279 plan.inverse(&mut v);
281 let offset = n * m / 2;
282 for (i, h) in self.taps.iter().enumerate() {
283 s[offset + i] += v[(i + m * self.overlap - shift) % m] * (h * m as f64);
284 }
285 }
286 Ok(s)
287 }
288
289 pub fn demodulate(&self, r: &[C64]) -> Result<Vec<C64>> {
296 let m = self.n_fft;
297 let half_symbols = self.half_symbols(r.len())?;
298 let shift = (self.taps.len() - 1) / 2;
299 let plan = fft::plan(m);
300 let mut y = Vec::with_capacity(half_symbols * m);
301 let mut w = vec![C64::new(0.0, 0.0); m];
302 for n in 0..half_symbols {
303 w.fill(C64::new(0.0, 0.0));
304 let offset = n * m / 2;
305 for (i, h) in self.taps.iter().enumerate() {
306 w[(i + m * self.overlap - shift) % m] += r[offset + i] * *h;
307 }
308 plan.forward(&mut w);
309 for (k, wk) in w.iter().enumerate() {
310 y.push(wk * oqam_phase(k + n).conj());
311 }
312 }
313 Ok(y)
314 }
315}
316
317impl Modulate for Fbmc {
321 fn modulate_into(&self, x: &Symbols, out: &mut Signal) -> Result<()> {
322 let a = oqam_stagger(crate::one_stream(x, "FBMC")?, self.n_fft)?;
323 out.set_stream(&Fbmc::modulate(self, &a)?, self.sample_rate);
324 Ok(())
325 }
326}
327
328impl Demodulate for Fbmc {
331 fn demodulate_into(&mut self, y: &Signal, out: &mut Symbols) -> Result<()> {
336 check_input(y, self.sample_rate, "FBMC")?;
337 let re: Vec<f64> = Fbmc::demodulate(self, y.stream(0))?.iter().map(|v| v.re).collect();
338 out.set_stream(&oqam_destagger(&re, self.n_fft)?);
339 Ok(())
340 }
341}
342
343pub fn oqam_stagger(c: &[C64], m: usize) -> Result<Vec<f64>> {
351 ensure_shape(m > 0 && c.len() % m == 0, || {
352 format!("{} symbols are not whole symbols of {m} subcarriers", c.len())
353 })?;
354 let mut a = vec![0.0; 2 * c.len()];
355 for (k, row) in c.chunks_exact(m).enumerate() {
356 for (sub, v) in row.iter().enumerate() {
357 let (first, second) = if sub % 2 == 0 { (v.re, v.im) } else { (v.im, v.re) };
358 a[2 * k * m + sub] = first;
359 a[(2 * k + 1) * m + sub] = second;
360 }
361 }
362 Ok(a)
363}
364
365pub fn oqam_destagger(a: &[f64], m: usize) -> Result<Vec<C64>> {
372 ensure_shape(m > 0 && a.len() % (2 * m) == 0, || {
373 format!("{} real symbols are not an even number of half-symbols of {m} subcarriers", a.len())
374 })?;
375 let mut c = Vec::with_capacity(a.len() / 2);
376 for pair in a.chunks_exact(2 * m) {
377 let (first, second) = pair.split_at(m);
378 for sub in 0..m {
379 c.push(if sub % 2 == 0 { C64::new(first[sub], second[sub]) } else { C64::new(second[sub], first[sub]) });
380 }
381 }
382 Ok(c)
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use comnum_core::rng::Rng;
389
390 #[test]
394 fn the_shared_phase_and_normalisation_are_the_former_ones() {
395 let f = Fbmc::new(16, 4).unwrap();
396 let mut r = Rng::new(6);
397 let a: Vec<f64> = (0..16 * 10).map(|_| r.gaussian()).collect();
398 let (m, shift) = (16usize, (f.taps().len() - 1) / 2);
399 let plan = fft::plan(m);
400 let mut s = vec![C64::new(0.0, 0.0); f.n_samples(10)];
401 for (n, row) in a.chunks_exact(m).enumerate() {
402 let mut v: Vec<C64> = row
403 .iter()
404 .enumerate()
405 .map(|(k, ak)| cis(std::f64::consts::FRAC_PI_2 * ((k + n) % 4) as f64) * *ak)
406 .collect();
407 plan.inverse(&mut v);
408 for (i, h) in f.taps().iter().enumerate() {
409 s[n * m / 2 + i] += v[(i + m * 4 - shift) % m] * (h * m as f64);
410 }
411 }
412 assert_eq!(f.modulate(&a).unwrap(), s);
413 let y = f.demodulate(&s).unwrap();
414 for n in 0..10 {
415 let mut w = vec![C64::new(0.0, 0.0); m];
416 for (i, h) in f.taps().iter().enumerate() {
417 w[(i + m * 4 - shift) % m] += s[n * m / 2 + i] * *h;
418 }
419 plan.forward(&mut w);
420 for (k, wk) in w.iter().enumerate() {
421 assert_eq!(y[n * m + k], wk * cis(-std::f64::consts::FRAC_PI_2 * ((k + n) % 4) as f64));
422 }
423 }
424 let raw = Prototype::Phydyas.taps(16, 4).unwrap();
425 let norm = raw.iter().map(|v| v * v).sum::<f64>().sqrt();
426 assert!((norm - 1.0).abs() < 1e-15);
427 }
428
429 #[test]
430 fn the_traits_stagger_inside() {
431 let f = Fbmc::new(16, 4).unwrap().with_sample_rate(Hertz(1e6)).unwrap();
432 let c: Vec<C64> = (0..16 * 6)
433 .map(|i| C64::new(if i % 3 == 0 { 1.0 } else { -1.0 }, if i % 5 == 0 { -1.0 } else { 1.0 }))
434 .collect();
435 let x = Modulate::modulate(&f, &Symbols::from_stream(c.clone()).unwrap()).unwrap();
436 assert_eq!((x.len(), x.sample_rate()), (f.n_samples(12), Hertz(1e6)));
437 assert_eq!(x.stream(0), &f.modulate(&oqam_stagger(&c, 16).unwrap()).unwrap()[..]);
438 let mut g = f.clone();
439 let back = Demodulate::demodulate(&mut g, &x).unwrap();
440 assert!(back.as_slice().iter().zip(&c).all(|(a, b)| (a - b).norm() < 1e-2));
441 let other = Signal::from_stream(x.stream(0).to_vec(), Hertz(2e6)).unwrap();
442 assert!(Demodulate::demodulate(&mut g, &other).is_err() && f.clone().with_sample_rate(Hertz(-1.0)).is_err());
443 }
444
445 #[test]
446 fn distant_pulses_do_not_interfere() {
447 let f = Fbmc::new(8, 4).unwrap();
448 assert_eq!(f.interference(0, 0, 1, -1000), C64::new(0.0, 0.0));
449 assert_eq!(f.interference(0, 0, 1, isize::MAX), C64::new(0.0, 0.0));
450 assert!(f.interference(0, 0, 1, 7).norm() > 0.0);
451 }
452
453 #[test]
454 fn the_pulses_are_real_orthogonal_and_of_unit_energy() {
455 for (m, k) in [(8usize, 2usize), (16, 3), (32, 4)] {
456 let f = Fbmc::new(m, k).unwrap();
457 assert!((f.taps().iter().map(|v| v * v).sum::<f64>() - 1.0).abs() < 1e-12);
458 for m0 in [0usize, 1, 5] {
459 for n0 in [0usize, 1, 2, 3] {
460 for dm in -2isize..=2 {
461 for dn in -(2 * k as isize)..=(2 * k as isize) {
462 let v = f.interference(m0, n0, dm, dn);
463 let want = if dm == 0 && dn == 0 { 1.0 } else { 0.0 };
464 let tol = match k {
467 2 => 0.05,
468 3 => 5e-3,
469 _ => 5e-4,
470 };
471 assert!((v.re - want).abs() < tol, "M {m} K {k} ({m0},{n0}) + ({dm},{dn}): {v}");
472 }
473 }
474 }
475 }
476 }
477 }
478
479 #[test]
480 fn modulation_is_the_sum_of_the_pulses_and_demodulation_their_projection() {
481 let f = Fbmc::new(8, 4).unwrap();
482 let mut r = Rng::new(3);
483 let n = 5;
484 let a: Vec<f64> = (0..8 * n).map(|_| r.gaussian()).collect();
485 let s = f.modulate(&a).unwrap();
486 let mut direct = vec![C64::new(0.0, 0.0); f.n_samples(n)];
487 for nn in 0..n {
488 for m in 0..8 {
489 let (start, g) = f.pulse(m, nn);
490 for (i, v) in g.iter().enumerate() {
491 direct[start + i] += v * a[nn * 8 + m];
492 }
493 }
494 }
495 assert!(s.iter().zip(&direct).all(|(x, y)| (x - y).norm() < 1e-12));
496 let y = f.demodulate(&s).unwrap();
497 for nn in 0..n {
498 for m in 0..8 {
499 let (start, g) = f.pulse(m, nn);
500 let proj: C64 = g.iter().enumerate().map(|(i, v)| s[start + i] * v.conj()).sum();
501 assert!((y[nn * 8 + m] - proj).norm() < 1e-12);
502 }
503 }
504 }
505
506 #[test]
507 fn unit_power_and_half_the_noise_on_each_real_estimate() {
508 let (m, n) = (64usize, 400usize);
509 let f = Fbmc::new(m, 4).unwrap();
510 let mut r = Rng::new(9);
511 let a: Vec<f64> = (0..m * n).map(|_| if r.below(2) == 0 { 0.5f64.sqrt() } else { -0.5f64.sqrt() }).collect();
512 let s = f.modulate(&a).unwrap();
513 let mid = &s[f.taps().len()..s.len() - f.taps().len()];
515 let p = mid.iter().map(C64::norm_sqr).sum::<f64>() / mid.len() as f64;
516 assert!((p - 1.0).abs() < 0.02, "{p}");
517 let sigma2: f64 = 0.1;
518 let noisy: Vec<C64> = s.iter().map(|v| v + r.complex_gaussian() * sigma2.sqrt()).collect();
519 let y = f.demodulate(&noisy).unwrap();
520 let err: Vec<f64> = y.iter().zip(&a).map(|(v, a)| v.re - a).collect();
521 let var = err.iter().map(|e| e * e).sum::<f64>() / err.len() as f64;
522 assert!((var / (sigma2 / 2.0) - 1.0).abs() < 0.03, "{var}");
523 }
524
525 #[test]
526 fn the_spectrum_is_confined_where_that_of_ofdm_is_not() {
527 let (m, n) = (32usize, 16usize);
532 let f = Fbmc::new(m, 4).unwrap();
533 let mut a = vec![0.0; m * n];
534 let mut r = Rng::new(4);
535 let mut p = vec![0.0; 16 * m];
536 for _ in 0..50 {
537 for nn in 0..n {
538 a[nn * m] = if r.below(2) == 0 { 1.0 } else { -1.0 };
539 }
540 let mut s = f.modulate(&a).unwrap();
541 s.resize(16 * m * 8, C64::new(0.0, 0.0));
542 let spec = fft::fft(&s);
543 for (i, v) in p.iter_mut().enumerate() {
545 *v += spec[i * 8].norm_sqr();
546 }
547 }
548 let at = |k: f64| p[((k * 16.0).round() as isize).rem_euclid(16 * m as isize) as usize];
549 let fbmc_db = comnum_math::lin2db(at(8.5) / at(0.0));
550 let ofdm_db = 20.0 * comnum_math::sinc(8.5).abs().log10();
552 assert!(fbmc_db < -50.0 && ofdm_db > -30.0, "FBMC {fbmc_db} dB, OFDM {ofdm_db} dB");
553 }
554
555 #[test]
556 fn staggering_round_trips() {
557 let c: Vec<C64> = (0..12).map(|i| C64::new(i as f64, -(i as f64) - 0.5)).collect();
558 let a = oqam_stagger(&c, 4).unwrap();
559 assert_eq!(a.len(), 24);
560 assert_eq!((a[0], a[4], a[1], a[5]), (0.0, -0.5, -1.5, 1.0));
561 assert_eq!(oqam_destagger(&a, 4).unwrap(), c);
562 assert!(oqam_stagger(&c[..5], 4).is_err() && oqam_destagger(&a[..20], 4).is_err());
563 }
564}
565
566#[cfg(test)]
567mod literature {
568 use super::*;
569
570 #[test]
577 fn bouhadda_2014_table_1() {
578 let f = Fbmc::new(64, 4).unwrap();
579 let rows: [(isize, [f64; 7]); 3] = [
580 (1, [0.043, 0.125, 0.206, 0.239, 0.206, 0.125, 0.043]),
581 (0, [-0.067, 0.0, -0.564, 0.0, 0.564, 0.0, 0.067]),
582 (-1, [0.043, -0.125, 0.206, -0.239, 0.206, -0.125, 0.043]),
583 ];
584 for (dm, row) in rows {
585 for (j, want) in row.iter().enumerate() {
586 let dn = j as isize - 3;
587 let v = f.interference(10, 10, dm, dn);
588 if dm == 0 && dn == 0 {
589 assert!((v - 1.0).norm() < 1e-3);
590 } else {
591 assert!(v.re.abs() < 5e-4 && (v.im - want).abs() < 5e-4, "({dm}, {dn}): {v} against {want}j");
592 }
593 }
594 }
595 }
596}