comnum_waveform/ofdm/budget.rs
1//! The SNR-loss budget of an OFDM receiver (Speth, Fechtel, Fock & Meyr 1999).
2//!
3//! In tracking mode every residual imperfection of the inner receiver, a noisy channel
4//! estimate, a timing offset, a carrier frequency offset, fading within a symbol, is
5//! a rotation of the subcarriers plus an additive noise of power $\sigma^{2}$ (relative to
6//! the power of a subcarrier, $E|a_{k}|^{2} = E|H_{k}|^{2} = 1$), and its cost is the
7//! loss of SNR it causes, their eq. (14),
8//!
9//! $$
10//! \Delta\gamma = \frac{\gamma_{\mathrm{ideal}}}{\gamma_{\mathrm{real}}}, \qquad
11//! \sigma_{\mathrm{res}}^{2} = \sigma_{\varepsilon}^{2} + \sigma_{\Omega}^{2} +
12//! \sigma_{\mathrm{fad}}^{2} < \frac{1}{\gamma}\left(1 - \frac{1}{\Delta\gamma_{\max}}\right)
13//! $$
14//!
15//! (eqs. (50)–(51), $\gamma$ the SNR per subcarrier): the noises add, and a loss
16//! $\Delta\gamma_{\max}$ allows the total [`noise_budget`]. The noises of the
17//! imperfections:
18//!
19//! - a channel estimate of gain $G$ (its error is the noise divided by $G$, eq. (19)):
20//! $\Delta\gamma = 1 + 1/G$, eq. (20), [`estimator_loss_db`] and [`estimator_gain`];
21//! - a timing offset of $n_{\varepsilon}$ samples beyond the part of the guard interval
22//! that the echoes leave free, eqs. (30)–(31), [`timing_noise_power`];
23//! - a carrier frequency offset $\Delta f T_{u}$ (in subcarrier spacings), eq. (42),
24//! [`cfo_noise_power`], $\frac{\pi^{2}}{3}(\Delta f T_{u})^{2}$, and the largest one
25//! for a loss, eq. (43), [`max_cfo`];
26//! - fading of maximum Doppler $f_{D}$ within a symbol (Jakes), eq. (47),
27//! [`doppler_noise_power`], $\frac{\pi^{2}}{6}(f_{D} T_{u})^{2}$, and the largest
28//! Doppler for a loss, [`max_doppler`] (the curves of their Fig. 7).
29//!
30//! The approximations are those of the paper: one imperfection at a time, small errors
31//! (the attenuation of the wanted subcarrier neglected), every interference Gaussian;
32//! [`super::sync::ici_power`] gives the exact interference of a frequency offset.
33//!
34//! # Examples
35//! ```
36//! use comnum_waveform::ofdm::budget::{estimator_gain, estimator_loss_db, max_doppler};
37//! // Sec. IV-A: an estimator of gain 10 costs 0.41 dB; 0.1 dB needs a gain of 43
38//! assert!((estimator_loss_db(10.0) - 0.414).abs() < 1e-3);
39//! assert!((estimator_gain(0.1) - 42.9).abs() < 0.05);
40//! // Fig. 7: at 20 dB of SNR, 1 dB of loss tolerates f_D T_u = 0.035
41//! assert!((max_doppler(100.0, 1.0) - 0.0354).abs() < 1e-4);
42//! ```
43//!
44//! # References
45//!
46//! 1. M. Speth, S. A. Fechtel, G. Fock and H. Meyr, "Optimum receiver design for wireless
47//! broad-band systems using OFDM — Part I," *IEEE Trans. Commun.*, 47(11),
48//! 1668–1677, 1999.
49
50use comnum_core::{Result, ensure};
51use std::f64::consts::PI;
52
53/// The SNR loss (dB) of a receiver that equalises with a channel estimate of gain $G$,
54/// $10 \log_{10}(1 + 1/G)$ (eq. (20)).
55pub fn estimator_loss_db(gain: f64) -> f64 {
56 10.0 * (1.0 + 1.0 / gain).log10()
57}
58
59/// The gain $G = 1/(\Delta\gamma - 1)$ a channel estimator needs for a loss of `loss_db`
60/// (dB): the inverse of [`estimator_loss_db`] (their Fig. 2).
61pub fn estimator_gain(loss_db: f64) -> f64 {
62 1.0 / (10f64.powf(loss_db / 10.0) - 1.0)
63}
64
65/// The largest total power $\sigma_{\mathrm{res}}^{2}$ of the residual noises (relative to
66/// the power of a subcarrier) for a loss of `loss_db` (dB) at the SNR `snr_lin` (linear,
67/// per subcarrier): $(1 - 1/\Delta\gamma_{\max})/\gamma$ (eqs. (32), (51)).
68pub fn noise_budget(snr_lin: f64, loss_db: f64) -> f64 {
69 (1.0 - 10f64.powf(-loss_db / 10.0)) / snr_lin
70}
71
72/// The power of the intersymbol and intercarrier interference of a timing offset
73/// (eqs. (30)–(31)): over the taps `(delay, power)` of the channel (delays in samples,
74/// powers of unit sum), $\sum_{i} p_{i}\,(2\Delta_{i}/N - (\Delta_{i}/N)^{2})$, with
75/// $\Delta_{i}$ the samples of the FFT window of $N$ = `n_fft` points that fall outside
76/// the symbol of tap $i$ for a window `offset` samples late (positive) or early
77/// (negative) of the end of the guard interval of `guard` samples:
78/// $\Delta_{i} = n_{\varepsilon} - \tau_{i}$ if $n_{\varepsilon} > \tau_{i}$,
79/// $\tau_{i} - N_{g} - n_{\varepsilon}$ if $n_{\varepsilon} < \tau_{i} - N_{g}$, 0
80/// otherwise (the second condition is printed $0 < n_{\varepsilon}T < -(T_{g} -
81/// \tau_{i})$ in the paper, which cannot hold for an echo inside the guard interval).
82///
83/// # Errors
84/// [`comnum_core::Error::Config`] for an empty FFT, a negative delay or power.
85pub fn timing_noise_power(offset: f64, n_fft: usize, guard: usize, taps: &[(f64, f64)]) -> Result<f64> {
86 ensure(n_fft > 0, || "the FFT needs at least one point".into())?;
87 ensure(taps.iter().all(|&(d, p)| d >= 0.0 && p >= 0.0), || "the delays and powers must be non-negative".into())?;
88 let n = n_fft as f64;
89 Ok(taps
90 .iter()
91 .map(|&(tau, p)| {
92 let excess = if offset > tau {
93 offset - tau
94 } else if offset < tau - guard as f64 {
95 tau - guard as f64 - offset
96 } else {
97 0.0
98 };
99 let x = excess.min(n) / n;
100 p * (2.0 * x - x * x)
101 })
102 .sum())
103}
104
105/// The power of the intercarrier interference of a small carrier frequency offset
106/// `eps` (in subcarrier spacings), $\frac{\pi^{2}}{3}\varepsilon^{2}$ (eq. (42)): the
107/// second-order term of [`super::sync::ici_power`] when every subcarrier is active.
108pub fn cfo_noise_power(eps: f64) -> f64 {
109 PI * PI / 3.0 * eps * eps
110}
111
112/// The power of the intercarrier interference of fading within a symbol, for a Jakes
113/// spectrum of maximum Doppler `fd_tu` (normalised to the subcarrier spacing,
114/// $f_{D} T_{u}$), $\frac{\pi^{2}}{6}(f_{D} T_{u})^{2}$ (eq. (47); taps of unit total
115/// power).
116pub fn doppler_noise_power(fd_tu: f64) -> f64 {
117 PI * PI / 6.0 * fd_tu * fd_tu
118}
119
120/// The signal-to-interference ratio of CP-OFDM under Jakes' fading of maximum Doppler
121/// `fd_tu` (normalised to the subcarrier spacing, $f_{D} T_{u}$), the prefix covering the
122/// delay spread (no ISI), exact (Robertson & Kaiser 1999; Nissel, Schwarz & Rupp, *IEEE J.
123/// Sel. Areas Commun.* 35(8), 2017, eq. (42)):
124///
125/// $$
126/// \mathrm{SIR} = \frac{{}_{1}F_{2}\left(\tfrac{1}{2}; \tfrac{3}{2}, 2; -(\pi f_{D} T_{u})^{2}\right)}
127/// {1 - {}_{1}F_{2}\left(\tfrac{1}{2}; \tfrac{3}{2}, 2; -(\pi f_{D} T_{u})^{2}\right)},
128/// $$
129///
130/// the numerator the power kept on the subcarrier, the denominator the ICI, of a channel
131/// of unit power. [`doppler_noise_power`] is its small-Doppler limit, $1/\mathrm{SIR}
132/// \to \frac{\pi^{2}}{6}(f_{D} T_{u})^{2}$. With the prefix just covering the delay spread
133/// $\tau_{\max}$, $\kappa = T_{\mathrm{cp}}/T_{u}$, $f_{D} T_{u} = f_{D} \tau_{\max}/\kappa$.
134///
135/// # Examples
136/// ```
137/// use comnum_waveform::ofdm::budget::{doppler_noise_power, jakes_ici_sir};
138/// let fd_tu = 0.01;
139/// assert!((1.0 / jakes_ici_sir(fd_tu) / doppler_noise_power(fd_tu) - 1.0).abs() < 1e-3);
140/// ```
141pub fn jakes_ici_sir(fd_tu: f64) -> f64 {
142 let x = PI * fd_tu;
143 let f = comnum_math::hyp1f2(0.5, 1.5, 2.0, -x * x);
144 f / (1.0 - f)
145}
146
147/// The largest carrier frequency offset (in subcarrier spacings) for a loss of
148/// `loss_db` (dB) at the SNR `snr_lin` (linear, per subcarrier), eq. (43):
149/// [`cfo_noise_power`] equal to the [`noise_budget`].
150pub fn max_cfo(snr_lin: f64, loss_db: f64) -> f64 {
151 (3.0 * noise_budget(snr_lin, loss_db)).sqrt() / PI
152}
153
154/// The largest maximum Doppler $f_{D} T_{u}$ (normalised to the subcarrier spacing) for a
155/// loss of `loss_db` (dB) at the SNR `snr_lin` (linear, per subcarrier): the bound of
156/// Fig. 7 of Speth et al., [`doppler_noise_power`] equal to the [`noise_budget`]
157/// ([`max_cfo`] times $\sqrt{2}$).
158pub fn max_doppler(snr_lin: f64, loss_db: f64) -> f64 {
159 (6.0 * noise_budget(snr_lin, loss_db)).sqrt() / PI
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::ofdm::Ofdm;
166 use crate::ofdm::sync::ici_power;
167
168 #[test]
169 fn the_cfo_closed_form_is_the_small_offset_limit_of_the_exact_ici() {
170 // all subcarriers active, a large FFT: Moose's exact sum tends to pi^2/3 eps^2
171 let ofdm = Ofdm { oversampling: 1, ..Ofdm::new(1024, 1024).unwrap() };
172 for eps in [0.005, 0.01, 0.02] {
173 let exact = ici_power(&ofdm, 512, eps);
174 assert!((exact / cfo_noise_power(eps) - 1.0).abs() < 0.01, "eps {eps}: {exact}");
175 }
176 }
177
178 #[test]
179 fn the_timing_noise_vanishes_inside_the_free_guard_interval() {
180 let taps = [(0.0, 0.5), (10.0, 0.5)];
181 // a window up to 54 samples early with a guard of 64 and an echo at 10 is free
182 for offset in [0.0, -20.0, -54.0] {
183 assert_eq!(timing_noise_power(offset, 2048, 64, &taps).unwrap(), 0.0);
184 }
185 assert!(timing_noise_power(-55.0, 2048, 64, &taps).unwrap() > 0.0);
186 assert!(timing_noise_power(1.0, 2048, 64, &taps).unwrap() > 0.0);
187 // one path, late by n: 2n/N - (n/N)^2
188 let n = 10.0;
189 let one = timing_noise_power(n, 2048, 0, &[(0.0, 1.0)]).unwrap();
190 assert!((one - (2.0 * n / 2048.0 - (n / 2048.0).powi(2))).abs() < 1e-15);
191 assert!(
192 timing_noise_power(1.0, 0, 0, &taps).is_err() && timing_noise_power(1.0, 8, 0, &[(-1.0, 1.0)]).is_err()
193 );
194 }
195
196 #[test]
197 fn the_budget_inverts_the_tolerances() {
198 let (snr, loss) = (10f64.powf(1.5), 0.5);
199 assert!((cfo_noise_power(max_cfo(snr, loss)) - noise_budget(snr, loss)).abs() < 1e-15);
200 assert!((doppler_noise_power(max_doppler(snr, loss)) - noise_budget(snr, loss)).abs() < 1e-15);
201 assert!((max_doppler(snr, loss) / max_cfo(snr, loss) - 2f64.sqrt()).abs() < 1e-14);
202 assert!((estimator_loss_db(estimator_gain(0.7)) - 0.7).abs() < 1e-12);
203 }
204}