Skip to main content

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}