use comnum_core::{Result, ensure};
use comnum_math::{C64, cis};
pub type Pulse = (usize, Vec<C64>);
#[derive(Clone, Debug, PartialEq)]
pub struct DoublySelective {
delays: Vec<usize>,
powers: Vec<f64>,
doppler: f64,
}
impl DoublySelective {
pub fn new(taps: &[(f64, f64)], sample_rate: f64, max_doppler_hz: f64) -> Result<DoublySelective> {
ensure(!taps.is_empty(), || "a channel needs at least one tap".into())?;
ensure(sample_rate > 0.0 && max_doppler_hz >= 0.0, || {
format!("sample rate {sample_rate} Hz and Doppler {max_doppler_hz} Hz: positive and non-negative")
})?;
let mut rounded: Vec<(usize, f64)> = Vec::with_capacity(taps.len());
for &(delay_s, power_db) in taps {
ensure(delay_s >= 0.0 && power_db.is_finite(), || {
format!("tap at {delay_s} s, {power_db} dB: a non-negative delay and a finite power")
})?;
let d = (delay_s * sample_rate).round() as usize;
let p = 10f64.powf(power_db / 10.0);
match rounded.iter_mut().find(|(dd, _)| *dd == d) {
Some((_, pp)) => *pp += p,
None => rounded.push((d, p)),
}
}
rounded.sort_by_key(|&(d, _)| d);
let total: f64 = rounded.iter().map(|(_, p)| p).sum();
Ok(DoublySelective {
delays: rounded.iter().map(|&(d, _)| d).collect(),
powers: rounded.iter().map(|&(_, p)| p / total).collect(),
doppler: max_doppler_hz / sample_rate,
})
}
pub fn delays(&self) -> &[usize] {
&self.delays
}
pub fn powers(&self) -> &[f64] {
&self.powers
}
pub fn doppler(&self) -> f64 {
self.doppler
}
fn nodes(&self, span: usize) -> usize {
if self.doppler == 0.0 {
1
} else {
64 + (2.0 * std::f64::consts::TAU * self.doppler * span as f64).ceil() as usize
}
}
fn omega(&self, rx: &Pulse, tx: &[Pulse]) -> Vec<Vec<C64>> {
let (rx_start, rx_values) = rx;
let rx_end = rx_start + rx_values.len();
let span = tx.iter().map(|(s, v)| s + v.len()).max().unwrap_or(0).max(rx_end);
let q = self.nodes(span);
let omegas: Vec<f64> = (0..q)
.map(|k| {
let theta = std::f64::consts::TAU * (k as f64 + 0.5) / q as f64;
std::f64::consts::TAU * self.doppler * theta.cos()
})
.collect();
let scale: Vec<f64> = self.powers.iter().map(|p| (p / q as f64).sqrt()).collect();
tx.iter()
.map(|(g_start, g)| {
let mut row = Vec::with_capacity(self.delays.len() * q);
for (m, &d) in self.delays.iter().enumerate() {
let start = (*rx_start).max(g_start + d);
let end = rx_end.min(g_start + d + g.len());
if start >= end {
row.extend(std::iter::repeat_n(C64::new(0.0, 0.0), q));
continue;
}
let c: Vec<C64> =
(start..end).map(|n| rx_values[n - rx_start].conj() * g[n - d - g_start]).collect();
for &w in &omegas {
let mut acc = C64::new(0.0, 0.0);
let step = cis(w);
let mut rot = cis(w * start as f64);
for v in &c {
acc += v * rot;
rot *= step;
}
row.push(acc * scale[m]);
}
}
row
})
.collect()
}
pub fn coefficient_power(&self, rx: &Pulse, tx: &Pulse) -> f64 {
self.omega(rx, std::slice::from_ref(tx))[0].iter().map(C64::norm_sqr).sum()
}
pub fn sir_complete(&self, rx: &Pulse, tx: &Pulse) -> f64 {
let signal = self.coefficient_power(rx, tx);
let total: f64 = rx.1.iter().map(C64::norm_sqr).sum();
let interference = (total - signal).max(0.0);
if interference == 0.0 { f64::INFINITY } else { signal / interference }
}
pub fn sir_qam(&self, rx: &Pulse, tx: &[Pulse], wanted: usize) -> f64 {
let omega = self.omega(rx, tx);
let gamma: Vec<f64> = omega.iter().map(|row| row.iter().map(C64::norm_sqr).sum()).collect();
ratio(&gamma, wanted)
}
pub fn sir_oqam(&self, rx: &Pulse, tx: &[Pulse], wanted: usize) -> f64 {
let omega = self.omega(rx, tx);
let phases: Vec<C64> = omega[wanted]
.iter()
.map(|v| if v.norm() > 0.0 { v.conj() / v.norm() } else { C64::new(1.0, 0.0) })
.collect();
let gamma: Vec<f64> =
omega.iter().map(|row| row.iter().zip(&phases).map(|(v, p)| (v * p).re.powi(2)).sum()).collect();
ratio(&gamma, wanted)
}
}
fn ratio(gamma: &[f64], wanted: usize) -> f64 {
let total: f64 = gamma.iter().sum();
let signal = gamma[wanted];
let interference = (total - signal).max(0.0);
if interference == 0.0 { f64::INFINITY } else { signal / interference }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fbmc::{Fbmc, Prototype};
use crate::ofdm::Ofdm;
use crate::ofdm::budget::jakes_ici_sir;
#[test]
fn flat_jakes_fading_on_cp_ofdm_gives_the_closed_form_sir() {
let n = 64;
let ofdm = Ofdm { n_cp: 4, oversampling: 1, ..Ofdm::new(n, n).unwrap() };
for fd_tu in [0.02, 0.1] {
let ch = DoublySelective::new(&[(0.0, 0.0)], 1.0, fd_tu / ofdm.fft_len() as f64).unwrap();
let tx: Vec<Pulse> =
(0..3).flat_map(|k| (0..n).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
let sir = ch.sir_qam(&ofdm.window(n / 2, 1), &tx, n + n / 2);
let want = jakes_ici_sir(fd_tu);
assert!((sir / want - 1.0).abs() < 2e-3, "{fd_tu}: {sir} against {want}");
}
}
#[test]
fn static_channels_give_no_interference_beyond_the_prototype() {
let ch = DoublySelective::new(&[(0.0, 0.0), (2.0, -3.0), (5.0, -10.0)], 1.0, 0.0).unwrap();
let ofdm = Ofdm { n_cp: 6, oversampling: 1, ..Ofdm::new(32, 32).unwrap() };
let tx: Vec<Pulse> = (0..3).flat_map(|k| (0..32).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
assert!(ch.sir_qam(&ofdm.window(3, 1), &tx, 32 + 3) > 1e20);
let flat = DoublySelective::new(&[(0.0, 0.0)], 1.0, 0.0).unwrap();
let fbmc = Fbmc::new(16, 4).unwrap();
let tx: Vec<Pulse> =
(0..17).flat_map(|n| (0..16).map(move |m| (m, n))).map(|(m, n)| fbmc.pulse(m, n)).collect();
let sir = flat.sir_oqam(&fbmc.pulse(8, 8), &tx, 8 * 16 + 8);
assert!(sir > 1e5, "{sir}");
let complex = flat.sir_qam(&fbmc.pulse(8, 8), &tx, 8 * 16 + 8);
assert!(complex > 0.5 && complex < 2.0, "{complex}");
}
#[test]
fn the_complete_basis_gives_the_sum_over_every_interferer() {
let ch = DoublySelective::new(&[(0.0, 0.0), (3.0, -4.0), (7.0, -9.0)], 1.0, 2e-3).unwrap();
let n = 32;
let ofdm = Ofdm { oversampling: 1, n_cp: 0, ..Ofdm::new(n, n).unwrap() };
let tx: Vec<Pulse> = (0..3).flat_map(|k| (0..n).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
let rx = ofdm.window(n / 2, 1);
let full = ch.sir_qam(&rx, &tx, n + n / 2);
let parseval = ch.sir_complete(&rx, &tx[n + n / 2]);
assert!((full / parseval - 1.0).abs() < 1e-9, "{full} {parseval}");
let fbmc = Fbmc::with_prototype(16, 4, Prototype::Hermite).unwrap();
let tx: Vec<Pulse> =
(0..17).flat_map(|k| (0..16).map(move |m| (m, k))).map(|(m, k)| fbmc.pulse(m, k)).collect();
let rx = fbmc.pulse(8, 8);
let full = ch.sir_oqam(&rx, &tx, 8 * 16 + 8);
let parseval = ch.sir_complete(&rx, &rx);
assert!((full / parseval - 1.0).abs() < 1e-4, "{full} {parseval}");
}
#[test]
fn taps_are_rounded_merged_and_normalised() {
let ch = DoublySelective::new(&[(0.0, 0.0), (0.4e-6, -3.0), (0.6e-6, -3.0)], 2e6, 100.0).unwrap();
assert_eq!(ch.delays(), &[0, 1]);
assert!((ch.powers().iter().sum::<f64>() - 1.0).abs() < 1e-12);
assert!((ch.powers()[1] / ch.powers()[0] - 10f64.powf(-0.3) * 2.0).abs() < 1e-9);
assert!((ch.doppler() - 5e-5).abs() < 1e-18);
assert!(DoublySelective::new(&[], 1.0, 0.0).is_err());
assert!(DoublySelective::new(&[(0.0, 0.0)], 1.0, -1.0).is_err());
}
}