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}