Skip to main content

comnum_waveform/ofdm/
correction.rs

1//! Pilot-based gain correction in the presence of a cochannel interferer (Cimini 1985,
2//! Sec. II-C to II-F).
3//!
4//! # Model
5//!
6//! Every sample of the desired signal $D$ (unit power) reaches the receiver through a
7//! flat Rayleigh gain $Z_{d} = A_{d} e^{j\theta_{d}}$, $\mathbb{E} A_{d}^{2} = 1$, with a
8//! cochannel interferer $I$ of independent gain $Z_{i}$, $\gamma = 1/\mathrm{SIR}$:
9//!
10//! $$
11//! R = Z_{d} D + \sqrt{\gamma}\, Z_{i} I .
12//! $$
13//!
14//! A pilot gives the complex envelope $Z_{p}$, equal to $Z_{d}$ in flat fading and
15//! correlated with it, $\mathbb{E}[Z_{d} Z_{p}^{*}] = \rho\, e^{j\phi}$, when the pilot
16//! is $s$ apart in frequency. The correction $\hat{D} = R / Z_{c}$ either follows the
17//! fades only down to a limit $\varepsilon$ (amplitude relative to the RMS gain),
18//! $Z_{c} = \max(A_{p}, \varepsilon)\, e^{j\theta_{p}}$ ([`gain_limited`], eq. (16)), or
19//! uses the optimum factor $Z_{c} = Z_{d}\left[1 + \gamma A_{i}^{2} / A_{d}^{2}\right]$
20//! of eq. (15), which needs the amplitude of the interferer. Unlimited correction
21//! ($\varepsilon = 0$) boosts the interferer in the deep fades of the desired signal.
22//!
23//! The signal-to-distortion ratio $\mathrm{SDR} = 1 / \mathbb{E}|\hat{D} - D|^{2}$ is, for
24//! gain-limited correction (eq. (25), and eq. (20) for $\rho = 1$, flat fading),
25//!
26//! $$
27//! \mathrm{SDR}^{-1} = \frac{\mathrm{SIR} + 1}{\mathrm{SIR}}\, \frac{1 - e^{-\varepsilon^{2}}}
28//! {\varepsilon^{2}} + 1 - \frac{\rho \sqrt{\pi} \cos\phi}{\varepsilon}
29//! \operatorname{erf}(\varepsilon) + \left(1 - \rho^{2} + \frac{1}{\mathrm{SIR}}\right)
30//! E_{1}(\varepsilon^{2}),
31//! $$
32//!
33//! [`sdr_gain_limited`], and for the optimum correction (eq. (21))
34//! $\mathrm{SDR}^{-1} = \frac{1}{\mathrm{SIR} - 1}\left(\frac{\mathrm{SIR}}{\mathrm{SIR} - 1}
35//! \ln \mathrm{SIR} - 1\right)$, [`sdr_optimum`].
36//!
37//! Three points of the paper do not hold as printed, and are corrected here:
38//!
39//! - eq. (19) lacks the $-1$ of the first term, $\overline{(A_{d} \min(1/A_{d},
40//!   1/\varepsilon) - 1)^{2}}$, without which eq. (20) does not follow;
41//! - the coherence bandwidth of the two-ray model of eq. (23), where $\rho^{2} = 0.5$, is
42//!   $s = \pi / (2\Delta)$ rad/s, $1/(4\Delta)$ Hz, not "$1/\Delta$";
43//! - the phase of the correlation of that model is $\phi = -s\Delta/2$ (a delay of
44//!   $\Delta/2$ on average); $\arctan(-s\sigma)$, $\sigma = \Delta/2$, is that of an
45//!   exponential delay profile. The two agree to $10^{-3}$ rad at the separations of
46//!   Fig. 7 (500 Hz and 1 kHz with $\Delta = 50$ µs).
47//!
48//! # Examples
49//! ```
50//! use comnum_waveform::ofdm::correction::{sdr_gain_limited, sdr_optimum};
51//! use comnum_math::C64;
52//! // SIR 20 dB: limiting the correction near -10 dB costs little against the optimum
53//! let best = sdr_gain_limited(100.0, 10f64.powf(-0.5), C64::new(1.0, 0.0));
54//! assert!(comnum_math::lin2db(best) > 13.0 && best < sdr_optimum(100.0));
55//! ```
56//!
57//! # References
58//!
59//! 1. L. J. Cimini, Jr., "Analysis and simulation of a digital mobile channel using
60//!    orthogonal frequency division multiplexing," *IEEE Trans. Commun.*, 33(7),
61//!    665–675, 1985.
62
63use comnum_math::{C64, erf, expint_e1};
64
65/// Gain-limited correction of eq. (16): $\hat{D} = R / (\max(|Z_{p}|, \varepsilon)
66/// e^{j \arg Z_{p}})$ for every sample of `r` and of the pilot envelope `pilot`
67/// (`eps`: the limit, an amplitude relative to the RMS gain; 0 for unlimited
68/// correction). A zero pilot with no limit gives a non-finite sample.
69pub fn gain_limited(r: &[C64], pilot: &[C64], eps: f64) -> Vec<C64> {
70    r.iter()
71        .zip(pilot)
72        .map(|(r, p)| {
73            let a = p.norm();
74            if a >= eps { r / p } else { r / (p * (eps / a)) }
75        })
76        .collect()
77}
78
79/// The SDR (linear) of gain-limited correction, eq. (25) of Cimini 1985 (see the
80/// module): `sir_lin` linear (infinite without interferer), `eps` the limit (amplitude
81/// relative to the RMS gain, positive), `rho` the complex correlation
82/// $\rho e^{j\phi} = \mathbb{E}[Z_{d} Z_{p}^{*}]$ of the data and pilot envelopes (1
83/// in flat fading, eq. (20)).
84pub fn sdr_gain_limited(sir_lin: f64, eps: f64, rho: C64) -> f64 {
85    let e2 = eps * eps;
86    let gamma = 1.0 / sir_lin;
87    let d = (1.0 + gamma) * -(-e2).exp_m1() / e2 + 1.0 - rho.re * std::f64::consts::PI.sqrt() * erf(eps) / eps
88        + (1.0 - rho.norm_sqr() + gamma) * expint_e1(e2);
89    1.0 / d
90}
91
92/// The SDR (linear) of the optimum gain correction, eq. (21) of Cimini 1985:
93/// $1 / \mathbb{E}\left[\gamma A_{i}^{2} / (A_{d}^{2} + \gamma A_{i}^{2})\right]$ for
94/// independent Rayleigh amplitudes (`sir_lin` linear; `sir_lin` = 1 gives 2, the limit).
95pub fn sdr_optimum(sir_lin: f64) -> f64 {
96    if (sir_lin - 1.0).abs() < 1e-6 {
97        // the expansion of eq. (21) at SIR = 1: 1/2 + (SIR - 1)/6 + …
98        return 1.0 / (0.5 - (sir_lin - 1.0) / 6.0);
99    }
100    let s = sir_lin - 1.0;
101    s / (sir_lin / s * sir_lin.ln() - 1.0)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use comnum_core::rng::Rng;
108
109    /// The mean squared distortion of a correction rule by Monte Carlo over independent
110    /// Rayleigh gains, the pilot of correlation `rho` with the data.
111    fn distortion(sir: f64, rho: C64, correct: impl Fn(C64, C64, C64, f64) -> C64) -> (f64, f64) {
112        let mut r = Rng::new(17);
113        let n: usize = 400_000;
114        let g = 1.0 / sir;
115        let c = comnum_mapping::constellation::Constellation::qam(4).unwrap().normalized();
116        let qpsk = |r: &mut Rng| c.points()[r.below(4)];
117        let v: Vec<f64> = (0..n)
118            .map(|_| {
119                let (zd, zi, zx) = (r.complex_gaussian(), r.complex_gaussian(), r.complex_gaussian());
120                // E[Zd Zp*] = rho: Zp = rho* Zd + sqrt(1 - |rho|²) Zx
121                let zp = rho.conj() * zd + (1.0 - rho.norm_sqr()).sqrt() * zx;
122                let (d, i) = (qpsk(&mut r), qpsk(&mut r));
123                let rx = zd * d + g.sqrt() * zi * i;
124                (correct(rx, zp, zd, zi.norm_sqr() * g) - d).norm_sqr()
125            })
126            .collect();
127        let mean = v.iter().sum::<f64>() / n as f64;
128        let sd = (v.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n * (n - 1)) as f64).sqrt();
129        (mean, sd)
130    }
131
132    #[test]
133    fn eq_25_is_the_distortion_of_gain_limited_correction() {
134        for (sir, eps, rho) in [
135            (100.0, 0.3, C64::new(1.0, 0.0)),
136            (10.0, 0.6, C64::new(1.0, 0.0)),
137            (f64::INFINITY, 0.2, C64::from_polar(0.95, -0.2)),
138            (30.0, 0.4, C64::from_polar(0.8, 0.5)),
139        ] {
140            let (d, sd) = distortion(sir, rho, |rx, zp, _, _| gain_limited(&[rx], &[zp], eps)[0]);
141            let want = 1.0 / sdr_gain_limited(sir, eps, rho);
142            assert!((d - want).abs() < 4.0 * sd, "SIR {sir}, ε {eps}: {d} ± {sd} vs {want}");
143        }
144    }
145
146    #[test]
147    fn eq_21_is_the_distortion_of_the_optimum_correction() {
148        for sir in [0.5, 1.0, 3.0, 100.0] {
149            // Z_c = Z_d (1 + γ A_i²/A_d²)
150            let (d, sd) =
151                distortion(sir, C64::new(1.0, 0.0), |rx, _, zd, gai2| rx / (zd * (1.0 + gai2 / zd.norm_sqr())));
152            let want = 1.0 / sdr_optimum(sir);
153            assert!((d - want).abs() < 4.0 * sd, "SIR {sir}: {d} ± {sd} vs {want}");
154        }
155        assert!((sdr_optimum(1.0 + 1e-9) - sdr_optimum(1.0 + 2e-6)).abs() < 1e-5);
156    }
157
158    #[test]
159    fn gain_limited_correction_follows_the_fades_down_to_the_limit() {
160        let r = [C64::new(0.5, 0.0), C64::new(0.0, 0.01)];
161        let p = [C64::new(0.0, 0.5), C64::new(0.0, 0.01)];
162        let d = gain_limited(&r, &p, 0.1);
163        assert!((d[0] - C64::new(0.0, -1.0)).norm() < 1e-15);
164        // |Z_p| = 0.01 < 0.1: divided by 0.1 e^{jπ/2}
165        assert!((d[1] - C64::new(0.1, 0.0)).norm() < 1e-15);
166    }
167}
168
169#[cfg(test)]
170mod literature {
171    use super::*;
172
173    fn db(x: f64) -> f64 {
174        comnum_math::lin2db(x)
175    }
176
177    /// L. J. Cimini, Jr., IEEE Trans. Commun. 33(7), 1985, Fig. 3 (eq. (20), the SDR
178    /// against the gain limit): the peaks read on the figure (17.9 dB at -16.7 dB for an
179    /// SIR of 25 dB, 13.8 at -11.6 for 20 dB, 10.1 at -7.2 for 15 dB, 6.9 at -3.3 for 10
180    /// dB) within 0.3 dB and 1 dB, the SDR at -30 dB (16.2, 11.2, 6.2, 1.2 dB) within
181    /// 0.2 dB; the dashed levels of the figure are the SDR of the optimum correction,
182    /// eq. (21), and Fig. 4 (eq. (21)) reads 3 dB at SIR 0 dB, 14.1 at 20 dB and 31 at 40
183    /// dB.
184    #[test]
185    fn cimini_1985_figs_3_4() {
186        let one = C64::new(1.0, 0.0);
187        let curve =
188            |sir_db: f64, e_db: f64| db(sdr_gain_limited(comnum_math::db2lin(sir_db), comnum_math::db2amp(e_db), one));
189        for (sir, peak, at, low) in
190            [(25.0, 17.9, -16.7, 16.2), (20.0, 13.8, -11.6, 11.2), (15.0, 10.1, -7.2, 6.2), (10.0, 6.9, -3.3, 1.2)]
191        {
192            let gains: Vec<f64> = (0..=400).map(|k| -30.0 + 0.1 * k as f64).collect();
193            let (k, v) = comnum_math::argmax(gains.iter().map(|&e| curve(sir, e))).unwrap();
194            let e = gains[k];
195            assert!((v - peak).abs() < 0.3 && (e - at).abs() < 1.0, "SIR {sir}: {v} dB at {e} dB");
196            assert!((curve(sir, -30.0) - low).abs() < 0.2, "SIR {sir}");
197            // the optimum lies above every gain limit, by less than 1 dB at the peak
198            let opt = db(sdr_optimum(comnum_math::db2lin(sir)));
199            assert!(opt > v && opt - v < 1.0, "SIR {sir}: {opt}");
200        }
201        for (sir, read) in [(0.0, 3.0), (20.0, 14.1), (40.0, 31.0)] {
202            assert!((db(sdr_optimum(comnum_math::db2lin(sir))) - read).abs() < 0.3, "SIR {sir}");
203        }
204    }
205}