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}