Skip to main content

comnum_waveform/
sir.rs

1//! Signal-to-interference ratio of a multicarrier scheme with a one-tap equaliser over a
2//! doubly-selective channel, in closed form from its pulses (Nissel, Schwarz & Rupp 2017,
3//! Sec. IV).
4//!
5//! # Model
6//!
7//! The transmit pulses $\mathbf{g}_{j}$ (the columns of $\mathbf{G}$) carry uncorrelated
8//! unit-power symbols $x_{j}$ through a time-variant channel $\[\mathbf{H}\]_{n,n'} = h\[n -
9//! n', n\]$ and the receiver projects on the pulse $\mathbf{q}_{i}$ of the wanted symbol
10//! (their eq. (34)):
11//!
12//! $$
13//! y_{i} = \sum_{j} \underbrace{\mathbf{q}_{i}^{H} \mathbf{H} \mathbf{g}_{j}}_{c_{j}}\, x_{j},
14//! \qquad \[\boldsymbol\Gamma\]_{j,j'} = \mathrm{E}\\{c_{j} c_{j'}^{*}\\}.
15//! $$
16//!
17//! The channel is WSSUS: taps of delays $d_{m}$ (samples) and powers $p_{m}$, independent,
18//! each with the Clarke/Jakes correlation $J_{0}(2\pi \nu_{\max} \Delta t\,(n - n'))$,
19//! $\nu_{\max} \Delta t$ the maximum Doppler shift in cycles per sample. $J_{0}(x) =
20//! \frac{1}{Q}\sum_{q} \cos(x \cos\theta_{q})$ on the midpoint rule, exactly to the
21//! machine precision for $Q \ge 64 + 2 x_{\max}$ (`comnum_math::bessel_j0`), so the
22//! channel is the finite sum of sinusoids $h\[m, n\] = \sum_{q} \sqrt{p_{m}/Q}\,
23//! e^{j 2\pi \nu_{\max} \Delta t \cos\theta_{q}\, n} z_{m,q}$, $z$ i.i.d. unit complex
24//! Gaussian, and $\boldsymbol\Gamma = \boldsymbol\Omega \boldsymbol\Omega^{H}$ (their eq.
25//! (37)) with the explicit factor
26//!
27//! $$
28//! \[\boldsymbol\Omega\]_{j,(m,q)} = \sqrt{\frac{p_{m}}{Q}} \sum_{n} q_{i}^{*}\[n\]\,
29//! e^{j 2\pi \nu_{\max}\Delta t \cos\theta_{q}\, n}\, g_{j}\[n - d_{m}\].
30//! $$
31//!
32//! For complex symbols (OFDM, FBMC-QAM), their eq. (35),
33//! $\mathrm{SIR}_{i} = \[\boldsymbol\Gamma\]_{i,i} / (\mathrm{tr}\,\boldsymbol\Gamma -
34//! \[\boldsymbol\Gamma\]_{i,i})$ ([`DoublySelective::sir_qam`]). For the real symbols of
35//! FBMC-OQAM the one-tap equaliser removes the phase of the wanted coefficient and keeps
36//! the real part: each column of $\boldsymbol\Omega$ is turned by the phase of its entry
37//! in row $i$ (eq. (38); a zero entry is left as it is), $\tilde{\boldsymbol\Gamma}_{i} =
38//! \Re\\{\tilde{\boldsymbol\Omega}_{i}\\} \Re\\{\tilde{\boldsymbol\Omega}_{i}\\}^{T}$ (eq.
39//! (39)) and the SIR is the same ratio of $\tilde{\boldsymbol\Gamma}_{i}$ (eq. (40),
40//! [`DoublySelective::sir_oqam`]). The receiver knows the one tap exactly; the noise is
41//! zero; the pulses are the sampled ones of the modems (`Ofdm::pulse`, `Ofdm::window`,
42//! `Fbmc::pulse`), the delays rounded to the sampling grid.
43//!
44//! When the transmit pulses form a complete orthonormal basis (OFDM without prefix, every
45//! subcarrier of every symbol) the received power is the channel's whatever the delays and
46//! the Doppler, by Parseval: $\mathrm{tr}\,\boldsymbol\Gamma = \sum_{m} p_{m}
47//! \|\mathbf{q}_{i}\|^{2}$, and the SIR needs the wanted coefficient only,
48//! $\mathrm{SIR}_{i} = \[\boldsymbol\Gamma\]_{i,i} / (\|\mathbf{q}_{i}\|^{2} -
49//! \[\boldsymbol\Gamma\]_{i,i})$ ([`DoublySelective::sir_complete`]). The same holds for
50//! the real symbols of FBMC-OQAM on its whole lattice, a complete real-orthonormal frame
51//! ($\Re\{\mathbf{G}^{H}\mathbf{G}\} = \mathbf{I}$, their eq. (25)), since $\sum_{j}
52//! (\Re\langle \mathbf{y}, \mathbf{g}_{j}\rangle)^{2} = \|\mathbf{y}\|^{2}$ for every
53//! $\mathbf{y}$ and the phases of eq. (38) leave $\[\tilde{\boldsymbol\Gamma}_{i}\]_{i,i} =
54//! \[\boldsymbol\Gamma\]_{i,i}$: no truncation of the interferers is needed, where the sums
55//! of eqs. (35) and (40) over a finite set miss the interference the channel spreads far
56//! from the wanted symbol (a rectangular window spreads its ISI evenly over every
57//! subcarrier).
58//!
59//! # Examples
60//! ```
61//! use comnum_waveform::sir::DoublySelective;
62//! use comnum_waveform::ofdm::Ofdm;
63//! // CP-OFDM over a static two-path channel inside the prefix: no interference
64//! let ofdm = Ofdm { n_cp: 8, oversampling: 1, ..Ofdm::new(32, 32)? };
65//! let ch = DoublySelective::new(&[(0.0, 0.0), (3.0, -6.0)], 1.0, 0.0)?;
66//! let tx: Vec<_> = (0..3).flat_map(|n| (0..32).map(move |i| (i, n))).map(|(i, n)| ofdm.pulse(i, n)).collect();
67//! let sir = ch.sir_qam(&ofdm.window(5, 1), &tx, 32 + 5);
68//! assert!(sir > 1e20);
69//! // without the prefix the paths beyond the window interfere
70//! let ofdm = Ofdm { oversampling: 1, ..Ofdm::new(32, 32)? };
71//! let tx: Vec<_> = (0..3).flat_map(|n| (0..32).map(move |i| (i, n))).map(|(i, n)| ofdm.pulse(i, n)).collect();
72//! let sir = ch.sir_qam(&ofdm.window(5, 1), &tx, 32 + 5);
73//! assert!(sir > 1.0 && sir < 1e3);
74//! # Ok::<(), comnum_core::Error>(())
75//! ```
76//!
77//! # References
78//! 1. R. Nissel, S. Schwarz and M. Rupp, "Filter bank multicarrier modulation schemes for
79//!    future mobile communications," *IEEE J. Sel. Areas Commun.*, 35(8), 1768–1782,
80//!    2017: Sec. IV, eqs. (34)–(40).
81
82use comnum_core::{Result, ensure};
83use comnum_math::{C64, cis};
84
85/// A pulse on its support: `(first sample, values)`, as the modems return them.
86pub type Pulse = (usize, Vec<C64>);
87
88/// A WSSUS channel of discrete taps with Jakes' Doppler, on the sampling grid (see the
89/// module).
90#[derive(Clone, Debug, PartialEq)]
91pub struct DoublySelective {
92    /// Delays of the taps (samples), distinct and increasing.
93    delays: Vec<usize>,
94    /// Powers of the taps (linear, unit sum).
95    powers: Vec<f64>,
96    /// The maximum Doppler shift in cycles per sample, $\nu_{\max}\Delta t$.
97    doppler: f64,
98}
99
100impl DoublySelective {
101    /// The channel of the taps `(delay in s, power in dB)` sampled at `sample_rate` (Hz),
102    /// the delays rounded to the nearest sample (taps that fall on the same sample add
103    /// their powers) and the powers normalised to a unit sum, with the maximum Doppler
104    /// shift `max_doppler_hz` (Hz, Jakes' spectrum; 0 for a static channel).
105    ///
106    /// # Errors
107    /// [`comnum_core::Error::Config`] without taps, for a negative delay, Doppler or sample rate, or a
108    /// power that is not finite.
109    pub fn new(taps: &[(f64, f64)], sample_rate: f64, max_doppler_hz: f64) -> Result<DoublySelective> {
110        ensure(!taps.is_empty(), || "a channel needs at least one tap".into())?;
111        ensure(sample_rate > 0.0 && max_doppler_hz >= 0.0, || {
112            format!("sample rate {sample_rate} Hz and Doppler {max_doppler_hz} Hz: positive and non-negative")
113        })?;
114        let mut rounded: Vec<(usize, f64)> = Vec::with_capacity(taps.len());
115        for &(delay_s, power_db) in taps {
116            ensure(delay_s >= 0.0 && power_db.is_finite(), || {
117                format!("tap at {delay_s} s, {power_db} dB: a non-negative delay and a finite power")
118            })?;
119            let d = (delay_s * sample_rate).round() as usize;
120            let p = 10f64.powf(power_db / 10.0);
121            match rounded.iter_mut().find(|(dd, _)| *dd == d) {
122                Some((_, pp)) => *pp += p,
123                None => rounded.push((d, p)),
124            }
125        }
126        rounded.sort_by_key(|&(d, _)| d);
127        let total: f64 = rounded.iter().map(|(_, p)| p).sum();
128        Ok(DoublySelective {
129            delays: rounded.iter().map(|&(d, _)| d).collect(),
130            powers: rounded.iter().map(|&(_, p)| p / total).collect(),
131            doppler: max_doppler_hz / sample_rate,
132        })
133    }
134
135    /// Delays of the taps on the sampling grid (samples).
136    pub fn delays(&self) -> &[usize] {
137        &self.delays
138    }
139
140    /// Powers of the taps (linear, unit sum).
141    pub fn powers(&self) -> &[f64] {
142        &self.powers
143    }
144
145    /// The maximum Doppler shift in cycles per sample.
146    pub fn doppler(&self) -> f64 {
147        self.doppler
148    }
149
150    /// The number of Doppler nodes of the sum of sinusoids for pulses spanning `span`
151    /// samples: $64 + 2 x_{\max}$, $x_{\max} = 2\pi \nu_{\max} \Delta t \cdot$ `span`
152    /// (1 for a static channel).
153    fn nodes(&self, span: usize) -> usize {
154        if self.doppler == 0.0 {
155            1
156        } else {
157            64 + (2.0 * std::f64::consts::TAU * self.doppler * span as f64).ceil() as usize
158        }
159    }
160
161    /// The factor $\boldsymbol\Omega$ of the module: one row per transmit pulse of `tx`,
162    /// the columns the (tap, Doppler node) pairs, for the receive pulse `rx`.
163    fn omega(&self, rx: &Pulse, tx: &[Pulse]) -> Vec<Vec<C64>> {
164        let (rx_start, rx_values) = rx;
165        let rx_end = rx_start + rx_values.len();
166        let span = tx.iter().map(|(s, v)| s + v.len()).max().unwrap_or(0).max(rx_end);
167        let q = self.nodes(span);
168        let omegas: Vec<f64> = (0..q)
169            .map(|k| {
170                let theta = std::f64::consts::TAU * (k as f64 + 0.5) / q as f64;
171                std::f64::consts::TAU * self.doppler * theta.cos()
172            })
173            .collect();
174        let scale: Vec<f64> = self.powers.iter().map(|p| (p / q as f64).sqrt()).collect();
175        tx.iter()
176            .map(|(g_start, g)| {
177                let mut row = Vec::with_capacity(self.delays.len() * q);
178                for (m, &d) in self.delays.iter().enumerate() {
179                    // the product sequence c[n] = conj(q_i[n]) g_j[n - d] on the overlap
180                    let start = (*rx_start).max(g_start + d);
181                    let end = rx_end.min(g_start + d + g.len());
182                    if start >= end {
183                        row.extend(std::iter::repeat_n(C64::new(0.0, 0.0), q));
184                        continue;
185                    }
186                    let c: Vec<C64> =
187                        (start..end).map(|n| rx_values[n - rx_start].conj() * g[n - d - g_start]).collect();
188                    for &w in &omegas {
189                        let mut acc = C64::new(0.0, 0.0);
190                        // the phase of the sample `start`, then one rotation per sample
191                        let step = cis(w);
192                        let mut rot = cis(w * start as f64);
193                        for v in &c {
194                            acc += v * rot;
195                            rot *= step;
196                        }
197                        row.push(acc * scale[m]);
198                    }
199                }
200                row
201            })
202            .collect()
203    }
204
205    /// The mean power $\[\boldsymbol\Gamma\]_{i,i} = \mathrm{E}|\mathbf{q}^{H}\mathbf{H}\mathbf{g}|^{2}$
206    /// of the one-tap coefficient of the pulse `tx` received on `rx`, for a channel of unit
207    /// power.
208    pub fn coefficient_power(&self, rx: &Pulse, tx: &Pulse) -> f64 {
209        self.omega(rx, std::slice::from_ref(tx))[0].iter().map(C64::norm_sqr).sum()
210    }
211
212    /// The SIR (linear) of the symbol carried by `tx`, received on `rx`, when the transmit
213    /// pulses form a complete orthonormal basis (OFDM without prefix) or, for real symbols,
214    /// a complete real-orthonormal frame (FBMC-OQAM on its whole lattice):
215    /// $\[\boldsymbol\Gamma\]_{i,i} / (\|\mathbf{q}\|^{2} - \[\boldsymbol\Gamma\]_{i,i})$
216    /// (see the module).
217    pub fn sir_complete(&self, rx: &Pulse, tx: &Pulse) -> f64 {
218        let signal = self.coefficient_power(rx, tx);
219        let total: f64 = rx.1.iter().map(C64::norm_sqr).sum();
220        let interference = (total - signal).max(0.0);
221        if interference == 0.0 { f64::INFINITY } else { signal / interference }
222    }
223
224    /// The SIR (linear) of the complex symbol carried by `tx[wanted]`, received on `rx`
225    /// with a one-tap equaliser, the other pulses of `tx` interfering (eq. (35)).
226    ///
227    /// # Panics
228    /// If `wanted` is not an index of `tx`.
229    pub fn sir_qam(&self, rx: &Pulse, tx: &[Pulse], wanted: usize) -> f64 {
230        let omega = self.omega(rx, tx);
231        let gamma: Vec<f64> = omega.iter().map(|row| row.iter().map(C64::norm_sqr).sum()).collect();
232        ratio(&gamma, wanted)
233    }
234
235    /// The SIR (linear) of the real symbol carried by `tx[wanted]`, received on `rx`, the
236    /// one tap's phase removed and the real part kept (eqs. (38)–(40)).
237    ///
238    /// # Panics
239    /// If `wanted` is not an index of `tx`.
240    pub fn sir_oqam(&self, rx: &Pulse, tx: &[Pulse], wanted: usize) -> f64 {
241        let omega = self.omega(rx, tx);
242        let phases: Vec<C64> = omega[wanted]
243            .iter()
244            .map(|v| if v.norm() > 0.0 { v.conj() / v.norm() } else { C64::new(1.0, 0.0) })
245            .collect();
246        let gamma: Vec<f64> =
247            omega.iter().map(|row| row.iter().zip(&phases).map(|(v, p)| (v * p).re.powi(2)).sum()).collect();
248        ratio(&gamma, wanted)
249    }
250}
251
252/// $\Gamma_{ii} / (\mathrm{tr}\,\Gamma - \Gamma_{ii})$ from the diagonal of $\Gamma$.
253fn ratio(gamma: &[f64], wanted: usize) -> f64 {
254    let total: f64 = gamma.iter().sum();
255    let signal = gamma[wanted];
256    let interference = (total - signal).max(0.0);
257    if interference == 0.0 { f64::INFINITY } else { signal / interference }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::fbmc::{Fbmc, Prototype};
264    use crate::ofdm::Ofdm;
265    use crate::ofdm::budget::jakes_ici_sir;
266
267    /// Flat Jakes fading on CP-OFDM: the matrix SIR is the closed form of eq. (42) at
268    /// $f_{D} T_{u}$ (the FFT of the window spans $T_{u}$).
269    #[test]
270    fn flat_jakes_fading_on_cp_ofdm_gives_the_closed_form_sir() {
271        let n = 64;
272        let ofdm = Ofdm { n_cp: 4, oversampling: 1, ..Ofdm::new(n, n).unwrap() };
273        for fd_tu in [0.02, 0.1] {
274            // the Doppler per sample: the useful symbol spans fft_len samples
275            let ch = DoublySelective::new(&[(0.0, 0.0)], 1.0, fd_tu / ofdm.fft_len() as f64).unwrap();
276            let tx: Vec<Pulse> =
277                (0..3).flat_map(|k| (0..n).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
278            let sir = ch.sir_qam(&ofdm.window(n / 2, 1), &tx, n + n / 2);
279            let want = jakes_ici_sir(fd_tu);
280            assert!((sir / want - 1.0).abs() < 2e-3, "{fd_tu}: {sir} against {want}");
281        }
282    }
283
284    /// A static channel inside the prefix leaves CP-OFDM free of interference and
285    /// FBMC/OQAM at its intrinsic level (PHYDYAS: a residual of a few 1e-4 per neighbour).
286    #[test]
287    fn static_channels_give_no_interference_beyond_the_prototype() {
288        let ch = DoublySelective::new(&[(0.0, 0.0), (2.0, -3.0), (5.0, -10.0)], 1.0, 0.0).unwrap();
289        let ofdm = Ofdm { n_cp: 6, oversampling: 1, ..Ofdm::new(32, 32).unwrap() };
290        let tx: Vec<Pulse> = (0..3).flat_map(|k| (0..32).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
291        assert!(ch.sir_qam(&ofdm.window(3, 1), &tx, 32 + 3) > 1e20);
292        let flat = DoublySelective::new(&[(0.0, 0.0)], 1.0, 0.0).unwrap();
293        let fbmc = Fbmc::new(16, 4).unwrap();
294        let tx: Vec<Pulse> =
295            (0..17).flat_map(|n| (0..16).map(move |m| (m, n))).map(|(m, n)| fbmc.pulse(m, n)).collect();
296        let sir = flat.sir_oqam(&fbmc.pulse(8, 8), &tx, 8 * 16 + 8);
297        assert!(sir > 1e5, "{sir}");
298        // the complex SIR of the OQAM lattice is about 1: the imaginary interference
299        let complex = flat.sir_qam(&fbmc.pulse(8, 8), &tx, 8 * 16 + 8);
300        assert!(complex > 0.5 && complex < 2.0, "{complex}");
301    }
302
303    /// With every subcarrier of OFDM, or the whole OQAM lattice, the sums of eqs. (35) and
304    /// (40) equal the Parseval form of `sir_complete`.
305    #[test]
306    fn the_complete_basis_gives_the_sum_over_every_interferer() {
307        let ch = DoublySelective::new(&[(0.0, 0.0), (3.0, -4.0), (7.0, -9.0)], 1.0, 2e-3).unwrap();
308        let n = 32;
309        let ofdm = Ofdm { oversampling: 1, n_cp: 0, ..Ofdm::new(n, n).unwrap() };
310        let tx: Vec<Pulse> = (0..3).flat_map(|k| (0..n).map(move |i| (i, k))).map(|(i, k)| ofdm.pulse(i, k)).collect();
311        let rx = ofdm.window(n / 2, 1);
312        let full = ch.sir_qam(&rx, &tx, n + n / 2);
313        let parseval = ch.sir_complete(&rx, &tx[n + n / 2]);
314        assert!((full / parseval - 1.0).abs() < 1e-9, "{full} {parseval}");
315        let fbmc = Fbmc::with_prototype(16, 4, Prototype::Hermite).unwrap();
316        let tx: Vec<Pulse> =
317            (0..17).flat_map(|k| (0..16).map(move |m| (m, k))).map(|(m, k)| fbmc.pulse(m, k)).collect();
318        let rx = fbmc.pulse(8, 8);
319        let full = ch.sir_oqam(&rx, &tx, 8 * 16 + 8);
320        let parseval = ch.sir_complete(&rx, &rx);
321        assert!((full / parseval - 1.0).abs() < 1e-4, "{full} {parseval}");
322    }
323
324    #[test]
325    fn taps_are_rounded_merged_and_normalised() {
326        let ch = DoublySelective::new(&[(0.0, 0.0), (0.4e-6, -3.0), (0.6e-6, -3.0)], 2e6, 100.0).unwrap();
327        assert_eq!(ch.delays(), &[0, 1]);
328        assert!((ch.powers().iter().sum::<f64>() - 1.0).abs() < 1e-12);
329        assert!((ch.powers()[1] / ch.powers()[0] - 10f64.powf(-0.3) * 2.0).abs() < 1e-9);
330        assert!((ch.doppler() - 5e-5).abs() < 1e-18);
331        assert!(DoublySelective::new(&[], 1.0, 0.0).is_err());
332        assert!(DoublySelective::new(&[(0.0, 0.0)], 1.0, -1.0).is_err());
333    }
334}