comnum_mimo/capacity.rs
1//! Capacity of a MIMO channel realisation.
2//!
3//! With the total transmit power $P$, the receiver noise variance $\sigma^{2}$ and
4//! $\rho = P / \sigma^{2}$:
5//!
6//! - without channel knowledge at the transmitter, equal power on every antenna:
7//!
8//! $$
9//! C = \log_{2} \det\left(I_{n_{r}} + \frac{\rho}{n_{t}} H H^{H}\right);
10//! $$
11//!
12//! - with channel knowledge, water-filling on the eigenmodes:
13//! $C = \sum_{i} \log_{2}(1 + p_{i} \sigma_{i}^{2})$ (see [`crate::precoding`]).
14//!
15//! The ergodic capacity of a fading channel is the mean of these over the channel
16//! realisations. Over the i.i.d. Rayleigh channel (entries $\mathcal{CN}(0, 1)$) it has
17//! the closed form of Telatar (1999, Theorem 2), [`ergodic_rayleigh`]: with
18//! $m = \min(n_{r}, n_{t})$, $n = \max(n_{r}, n_{t})$ and the associated Laguerre
19//! polynomials $L_{k}^{n-m}$,
20//!
21//! $$
22//! C = \int_{0}^{\infty} \log_{2}\left(1 + \frac{\rho}{n_{t}} \lambda\right)
23//! \sum_{k=0}^{m-1} \frac{k!}{(k+n-m)!} \left[L_{k}^{n-m}(\lambda)\right]^{2}
24//! \lambda^{n-m} e^{-\lambda} \, d\lambda ,
25//! $$
26//!
27//! the sum being $m$ times the density of an unordered eigenvalue of the Wishart matrix
28//! $H H^{H}$ or $H^{H} H$ ([`wishart_eigenvalue_density`]). [`ergodic_rayleigh_limit`]
29//! is its slope for $n_{r} = n_{t} \to \infty$ (eq. (13)). When $H$ is drawn once and
30//! held, the rate $R$ is in outage with the probability [`outage_rayleigh`]
31//! (eq. (16) and Example 7, for one antenna on one side), and [`outage_capacity_rayleigh`]
32//! is the largest rate of outage probability $\varepsilon$ (eqs. (17), (18)).
33//!
34//! # Examples
35//! ```
36//! use comnum_mimo::capacity::{equal_power, water_filling};
37//! use comnum_math::linalg::identity;
38//! // two parallel unit channels at ρ = 10: 2 log2(1 + 5)
39//! let h = identity(2);
40//! assert!((equal_power(&h.view(), 10.0)? - 2.0 * 6f64.log2()).abs() < 1e-12);
41//! assert!((water_filling(&h.view(), 10.0)? - 2.0 * 6f64.log2()).abs() < 1e-12);
42//! # Ok::<(), comnum_core::Error>(())
43//! ```
44//!
45//! # References
46//!
47//! 1. E. Telatar, "Capacity of multi-antenna Gaussian channels," *Eur. Trans.
48//! Telecommun.*, 10(6), 1999.
49//! 2. G. J. Foschini and M. J. Gans, "On limits of wireless communications in a fading
50//! environment when using multiple antennas," *Wireless Pers. Commun.*, 6(3), 1998.
51
52use crate::precoding;
53use comnum_core::{Result, ensure};
54use comnum_math::linalg::{hermitian, identity, log2_det_hpd, svd};
55use comnum_math::{C64, bisect, gamma_p, ln_gamma, simpson};
56use ndarray::ArrayView2;
57
58/// Capacity with equal power on the $n_{t}$ antennas (bits per channel use).
59///
60/// # Errors
61/// [`comnum_core::Error::Config`] for a negative SNR.
62pub fn equal_power(h: &ArrayView2<'_, C64>, snr_lin: f64) -> Result<f64> {
63 ensure(snr_lin >= 0.0, || format!("the SNR must be non-negative, got {snr_lin}"))?;
64 let a = identity(h.nrows()) + h.dot(&hermitian(h)).mapv(|x| x * (snr_lin / h.ncols() as f64));
65 log2_det_hpd(&a.view())
66}
67
68/// Capacity with water-filling on the eigenmodes (bits per channel use).
69///
70/// # Errors
71/// [`comnum_core::Error::Config`] for a non-positive SNR.
72pub fn water_filling(h: &ArrayView2<'_, C64>, snr_lin: f64) -> Result<f64> {
73 let (_, s, _) = svd(h);
74 let g: Vec<f64> = s.iter().map(|x| x * x).collect();
75 let p = precoding::water_filling_powers(&g, snr_lin)?;
76 Ok(g.iter().zip(&p).map(|(g, p)| (1.0 + g * p).log2()).sum())
77}
78
79/// The density of one unordered eigenvalue of the complex Wishart matrix $H H^{H}$,
80/// $H$ of size $m \times n$ ($m \le n$) with i.i.d. $\mathcal{CN}(0, 1)$ entries
81/// (Telatar 1999, eq. preceding (8)):
82///
83/// $$
84/// p(\lambda) = \frac{1}{m} \sum_{k=0}^{m-1} \frac{k!}{(k+n-m)!}
85/// \left[L_{k}^{n-m}(\lambda)\right]^{2} \lambda^{n-m} e^{-\lambda} .
86/// $$
87///
88/// The normalized Laguerre functions
89/// $\ell_{k} = \sqrt{k!/(k+\alpha)!}\, L_{k}^{\alpha}(\lambda)\, \lambda^{\alpha/2}
90/// e^{-\lambda/2}$, $\alpha = n - m$, are computed by the three-term recurrence
91/// $\sqrt{(k+1)(k+1+\alpha)}\, \ell_{k+1} = (2k+1+\alpha-\lambda)\, \ell_{k} -
92/// \sqrt{k(k+\alpha)}\, \ell_{k-1}$, which stays in range where the polynomials
93/// themselves overflow. `m` and `n` are swapped if `m > n`.
94///
95/// # Examples
96/// ```
97/// use comnum_mimo::capacity::wishart_eigenvalue_density;
98/// // one antenna on each side: |h|² is exponential
99/// assert!((wishart_eigenvalue_density(1, 1, 2.0) - (-2f64).exp()).abs() < 1e-15);
100/// ```
101pub fn wishart_eigenvalue_density(m: usize, n: usize, lambda: f64) -> f64 {
102 let (m, n) = (m.min(n), m.max(n));
103 if m == 0 || lambda < 0.0 {
104 return 0.0;
105 }
106 let alpha = (n - m) as f64;
107 let power = if alpha == 0.0 { 0.0 } else { 0.5 * alpha * lambda.ln() };
108 let mut prev = 0.0;
109 let mut cur = (power - 0.5 * lambda - 0.5 * ln_gamma(alpha + 1.0)).exp();
110 let mut sum = cur * cur;
111 for k in 0..m - 1 {
112 let k = k as f64;
113 let next = ((2.0 * k + 1.0 + alpha - lambda) * cur - (k * (k + alpha)).sqrt() * prev)
114 / ((k + 1.0) * (k + 1.0 + alpha)).sqrt();
115 (prev, cur) = (cur, next);
116 sum += cur * cur;
117 }
118 sum / m as f64
119}
120
121/// Ergodic capacity (bits per channel use) of the i.i.d. Rayleigh channel with `n_rx`
122/// receive and `n_tx` transmit antennas, the total SNR `snr_lin` (linear) spread equally
123/// on the transmit antennas, the channel known at the receiver only: Telatar (1999),
124/// Theorem 2, eq. (8), integrated by Simpson's rule in $\lambda$ up to well beyond the
125/// largest eigenvalue after the change of variable $\lambda = u^{2}$ (the Laguerre
126/// functions oscillate with a nearly constant period in $u$), to about $10^{-10}$
127/// relative. It is the mean of
128/// [`equal_power`] over the realisations of $H$.
129///
130/// # Errors
131/// [`comnum_core::Error::Config`] for no antenna or a negative SNR.
132///
133/// # Examples
134/// ```
135/// use comnum_mimo::capacity::ergodic_rayleigh;
136/// use comnum_math::expint_e1;
137/// // one antenna: e^{1/ρ} E₁(1/ρ) nats
138/// let rho = 10.0;
139/// let c = ergodic_rayleigh(1, 1, rho)?;
140/// assert!((c * 2f64.ln() - (1.0 / rho).exp() * expint_e1(1.0 / rho)).abs() < 1e-9);
141/// // four antennas on each side: about four times as much at high SNR
142/// assert!(ergodic_rayleigh(4, 4, 1e3)? > 3.7 * ergodic_rayleigh(1, 1, 1e3)?);
143/// # Ok::<(), comnum_core::Error>(())
144/// ```
145pub fn ergodic_rayleigh(n_rx: usize, n_tx: usize, snr_lin: f64) -> Result<f64> {
146 ensure(n_rx >= 1 && n_tx >= 1, || format!("need at least one antenna on each side, got {n_rx} × {n_tx}"))?;
147 ensure(snr_lin >= 0.0, || format!("the SNR must be non-negative, got {snr_lin}"))?;
148 let (m, n) = (n_rx.min(n_tx), n_rx.max(n_tx));
149 // the largest eigenvalue concentrates at (√n + √m)², with fluctuations of order
150 // √n + √m: the density is below e^{-40} beyond the upper limit
151 let edge = (n as f64).sqrt() + (m as f64).sqrt();
152 let upper = edge * edge + 10.0 * edge + 60.0;
153 // λ = u²: the Laguerre functions oscillate with a nearly constant period in u
154 let intervals = 400 + (120.0 * (upper * (m + n) as f64).sqrt()) as usize;
155 let g = snr_lin / n_tx as f64;
156 let c = simpson(
157 |u| 2.0 * u * (g * u * u).ln_1p() * wishart_eigenvalue_density(m, n, u * u),
158 0.0,
159 upper.sqrt(),
160 intervals,
161 );
162 Ok(m as f64 * c / std::f64::consts::LN_2)
163}
164
165/// The limit of $C / n$ (bits per channel use and per antenna) of
166/// [`ergodic_rayleigh`] with $n$ antennas on each side, $n \to \infty$, at the total
167/// SNR $\rho$ (Telatar 1999, eq. (13), from the quarter-circle law of the eigenvalues
168/// of $H H^{H} / n$):
169///
170/// $$
171/// \lim_{n \to \infty} \frac{C}{n} = \left[\ln \rho - 1 + \frac{\sqrt{1 + 4\rho} - 1}{2\rho} +
172/// 2 \tanh^{-1} \frac{1}{\sqrt{1 + 4\rho}}\right] \frac{1}{\ln 2} .
173/// $$
174///
175/// # Examples
176/// ```
177/// use comnum_mimo::capacity::{ergodic_rayleigh, ergodic_rayleigh_limit};
178/// // twenty antennas are already within 0.5 % of the limit at 20 dB
179/// let c = ergodic_rayleigh(20, 20, 100.0)? / 20.0;
180/// assert!((c / ergodic_rayleigh_limit(100.0) - 1.0).abs() < 5e-3);
181/// # Ok::<(), comnum_core::Error>(())
182/// ```
183pub fn ergodic_rayleigh_limit(snr_lin: f64) -> f64 {
184 let s = (1.0 + 4.0 * snr_lin).sqrt();
185 (snr_lin.ln() - 1.0 + (s - 1.0) / (2.0 * snr_lin) + 2.0 * (1.0 / s).atanh()) / std::f64::consts::LN_2
186}
187
188fn ensure_single_side(n_rx: usize, n_tx: usize) -> Result<()> {
189 ensure(n_rx >= 1 && n_tx >= 1 && n_rx.min(n_tx) == 1, || {
190 format!("the closed form needs one antenna on one side, got {n_rx} × {n_tx}")
191 })
192}
193
194/// The outage probability $P(\log_{2} \det(I + \frac{\rho}{n_{t}} H H^{H}) < R)$ of a
195/// rate `rate` (bits per channel use) over a Rayleigh channel drawn once, with equal
196/// power on the transmit antennas and one antenna on one side. $\lVert h \rVert^{2}$ is
197/// then a gamma variable of shape $k = \max(n_{r}, n_{t})$, and
198///
199/// $$
200/// P_{\mathrm{out}} = P\left(k, \frac{n_{t} (2^{R} - 1)}{\rho}\right),
201/// $$
202///
203/// the regularized incomplete gamma function [`comnum_math::gamma_p`] (Telatar 1999, eq.
204/// (16) for $n_{t} = 1$ and Example 7 for $n_{r} = 1$, there in nats).
205///
206/// # Errors
207/// [`comnum_core::Error::Config`] unless one side has a single antenna, or for a
208/// non-positive SNR.
209///
210/// # Examples
211/// ```
212/// use comnum_mimo::capacity::outage_rayleigh;
213/// // one antenna: 1 - e^{-(2^R - 1)/ρ}
214/// let p = outage_rayleigh(1, 1, 10.0, 1.0)?;
215/// assert!((p - (1.0 - (-0.1f64).exp())).abs() < 1e-15);
216/// # Ok::<(), comnum_core::Error>(())
217/// ```
218pub fn outage_rayleigh(n_rx: usize, n_tx: usize, snr_lin: f64, rate: f64) -> Result<f64> {
219 ensure_single_side(n_rx, n_tx)?;
220 ensure(snr_lin > 0.0, || format!("the SNR must be positive, got {snr_lin}"))?;
221 let k = n_rx.max(n_tx) as f64;
222 Ok(gamma_p(k, n_tx as f64 * (rate * std::f64::consts::LN_2).exp_m1() / snr_lin))
223}
224
225/// The $\varepsilon$-capacity (bits per channel use) of [`outage_rayleigh`]: the rate
226/// $R$ whose outage probability is `eps` (Telatar 1999, eqs. (17) and (18), Figs. 3 and
227/// 5), $R = \log_{2}(1 + \rho x / n_{t})$ with $P(k, x) = \varepsilon$ solved by
228/// bisection.
229///
230/// # Errors
231/// [`comnum_core::Error::Config`] unless one side has a single antenna, for a
232/// non-positive SNR or for `eps` outside $(0, 1)$.
233///
234/// # Examples
235/// ```
236/// use comnum_mimo::capacity::{outage_capacity_rayleigh, outage_rayleigh};
237/// let r = outage_capacity_rayleigh(4, 1, 100.0, 1e-3)?;
238/// assert!((outage_rayleigh(4, 1, 100.0, r)? / 1e-3 - 1.0).abs() < 1e-9);
239/// # Ok::<(), comnum_core::Error>(())
240/// ```
241pub fn outage_capacity_rayleigh(n_rx: usize, n_tx: usize, snr_lin: f64, eps: f64) -> Result<f64> {
242 ensure_single_side(n_rx, n_tx)?;
243 ensure(snr_lin > 0.0, || format!("the SNR must be positive, got {snr_lin}"))?;
244 ensure(eps > 0.0 && eps < 1.0, || format!("the outage probability must be in (0, 1), got {eps}"))?;
245 let k = n_rx.max(n_tx) as f64;
246 // P(k, x) < ε on the left of the quantile; search ln x for the dynamic range
247 let ln_x = bisect(|u| gamma_p(k, u.exp()) < eps, -800.0, 10.0 + 2.0 * k.ln() + k.sqrt(), 200);
248 Ok((snr_lin * ln_x.exp() / n_tx as f64).ln_1p() / std::f64::consts::LN_2)
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use comnum_core::rng::Rng;
255 use ndarray::Array2;
256
257 #[test]
258 fn water_filling_never_loses_and_matches_at_high_snr() {
259 let mut r = Rng::new(1);
260 for _ in 0..50 {
261 let h = Array2::from_shape_fn((3, 3), |_| r.complex_gaussian());
262 for snr in [0.1, 1.0, 100.0] {
263 assert!(water_filling(&h.view(), snr).unwrap() >= equal_power(&h.view(), snr).unwrap() - 1e-9);
264 }
265 let hi = 1e6;
266 let gap = water_filling(&h.view(), hi).unwrap() - equal_power(&h.view(), hi).unwrap();
267 assert!(gap < 0.01, "{gap}");
268 }
269 }
270
271 #[test]
272 fn the_wishart_density_is_normalized_with_the_mean_of_the_trace() {
273 // ∫ p = 1 and ∫ λ p = n (E tr W = m n)
274 for (m, n) in [(1, 1), (1, 4), (3, 3), (2, 7), (8, 8)] {
275 let up = 4.0 * (m + n) as f64 + 80.0;
276 let mass = comnum_math::simpson(|l| wishart_eigenvalue_density(m, n, l), 0.0, up, 20_000);
277 let mean = comnum_math::simpson(|l| l * wishart_eigenvalue_density(m, n, l), 0.0, up, 20_000);
278 assert!((mass - 1.0).abs() < 1e-8, "{m} × {n}: {mass}");
279 assert!((mean / n as f64 - 1.0).abs() < 1e-8, "{m} × {n}: {mean}");
280 }
281 }
282
283 #[test]
284 fn the_ergodic_integral_has_converged() {
285 // against the same integral on four times as many intervals
286 for (nr, nt, snr) in [(1usize, 1usize, 1e-2), (8, 8, 100.0), (3, 20, 1e4), (32, 32, 10.0)] {
287 let (m, n) = (nr.min(nt), nr.max(nt));
288 let up = ((n as f64).sqrt() + (m as f64).sqrt()).powi(2) * 2.0 + 200.0;
289 let g = snr / nt as f64;
290 let fine = comnum_math::simpson(
291 |u| 2.0 * u * (g * u * u).ln_1p() * wishart_eigenvalue_density(m, n, u * u),
292 0.0,
293 up.sqrt(),
294 100_000,
295 ) * m as f64
296 / std::f64::consts::LN_2;
297 let c = ergodic_rayleigh(nr, nt, snr).unwrap();
298 assert!((c / fine - 1.0).abs() < 1e-10, "{nr} × {nt}: {c} vs {fine}");
299 }
300 }
301
302 #[test]
303 fn the_ergodic_capacity_is_the_mean_of_the_realisations() {
304 let mut r = Rng::new(7);
305 for (nr, nt, snr) in [(2usize, 2usize, 10.0), (4, 2, 3.0), (2, 4, 100.0), (1, 3, 10.0)] {
306 let n = 20_000;
307 let v: Vec<f64> = (0..n)
308 .map(|_| {
309 let h = Array2::from_shape_fn((nr, nt), |_| r.complex_gaussian());
310 equal_power(&h.view(), snr).unwrap()
311 })
312 .collect();
313 let mean = v.iter().sum::<f64>() / n as f64;
314 let sd = (v.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n * (n - 1)) as f64).sqrt();
315 let c = ergodic_rayleigh(nr, nt, snr).unwrap();
316 assert!((c - mean).abs() < 4.0 * sd, "{nr} × {nt}: {c} vs {mean} ± {sd}");
317 }
318 }
319
320 #[test]
321 fn the_outage_probability_is_that_of_the_realisations() {
322 let mut r = Rng::new(3);
323 for (nr, nt, snr, rate) in [(1usize, 1usize, 10.0, 2.0), (4, 1, 3.0, 2.5), (1, 3, 30.0, 4.0)] {
324 let n = 50_000;
325 let hits = (0..n)
326 .filter(|_| {
327 let h = Array2::from_shape_fn((nr, nt), |_| r.complex_gaussian());
328 equal_power(&h.view(), snr).unwrap() < rate
329 })
330 .count();
331 let p = outage_rayleigh(nr, nt, snr, rate).unwrap();
332 let sd = (p * (1.0 - p) / n as f64).sqrt();
333 assert!((hits as f64 / n as f64 - p).abs() < 4.0 * sd, "{nr} × {nt}: {hits} vs {p}");
334 }
335 assert!(outage_rayleigh(2, 2, 10.0, 1.0).is_err());
336 assert!(outage_capacity_rayleigh(1, 2, 10.0, 1.0).is_err());
337 }
338
339 #[test]
340 fn the_capacity_of_a_rank_one_channel_is_one_mode() {
341 // H = a b^H: one mode of gain ‖a‖² ‖b‖²
342 let a = ndarray::arr2(&[[C64::new(1.0, 0.0)], [C64::new(0.0, 1.0)]]);
343 let b = ndarray::arr2(&[[C64::new(1.0, 0.0), C64::new(1.0, 0.0)]]);
344 let h = a.dot(&b);
345 let c = water_filling(&h.view(), 5.0).unwrap();
346 assert!((c - (1.0 + 5.0 * 4.0f64).log2()).abs() < 1e-12);
347 }
348}
349
350#[cfg(test)]
351mod literature {
352 use super::*;
353
354 const NATS: f64 = std::f64::consts::LN_2;
355
356 /// E. Telatar, "Capacity of multi-antenna Gaussian channels," Eur. Trans. Telecommun.
357 /// 10(6), 1999, Remark 3 (the reciprocity $C(a, b, Pb) = C(b, a, Pa)$, while
358 /// $C(a, b, P) \ne C(b, a, P)$) and Examples 3 and 4: with one transmit antenna the
359 /// capacity follows $\ln(1 + Pr)$ as $r$ grows, with one receive antenna it tends to
360 /// $\ln(1 + P)$ as $t$ grows.
361 #[test]
362 fn telatar_1999_remark_3_and_examples_3_4() {
363 for (a, b, p) in [(1usize, 4usize, 10.0), (2, 5, 3.0), (3, 6, 100.0)] {
364 let left = ergodic_rayleigh(a, b, p * b as f64).unwrap();
365 let right = ergodic_rayleigh(b, a, p * a as f64).unwrap();
366 assert!((left - right).abs() < 1e-9 * left, "{a}, {b}: {left} vs {right}");
367 assert!((ergodic_rayleigh(a, b, p).unwrap() - ergodic_rayleigh(b, a, p).unwrap()).abs() > 0.1);
368 }
369 let p = 10.0;
370 let gap = |r: usize| ergodic_rayleigh(r, 1, p).unwrap() * NATS - (p * r as f64).ln_1p();
371 assert!(gap(4).abs() > gap(16).abs() && gap(16).abs() > gap(64).abs() && gap(64).abs() < 0.01, "{}", gap(64));
372 let lim = ergodic_rayleigh(1, 200, p).unwrap() * NATS;
373 assert!((lim - p.ln_1p()).abs() < 0.01, "{lim}");
374 }
375
376 /// Telatar 1999, Fig. 1 ($1 \le r, t \le 20$, $P = 20$ dB): its five contours are
377 /// labelled 12.7, 25.3, 38, 50.7 and 63.4 nats, the multiples $k\, C_{\max} / 6$ of
378 /// the largest value, $C(20, 20) = 76.0$ nats by eq. (8); Fig. 2 and eq. (13): with
379 /// $r = t$ the capacity is nearly linear in $r$, $C(20, 20) / 20$ within 0.5 % of the
380 /// limit (13) at every $P$ from 0 to 35 dB.
381 #[test]
382 fn telatar_1999_figs_1_2_and_eq_13() {
383 let p = 100.0;
384 let cmax = ergodic_rayleigh(20, 20, p).unwrap() * NATS;
385 for (k, label) in [(1.0, 12.7), (2.0, 25.3), (3.0, 38.0), (4.0, 50.7), (5.0, 63.4)] {
386 assert!((k * cmax / 6.0 - label).abs() < 0.05, "{k}: {}", k * cmax / 6.0);
387 }
388 for t in [1usize, 7, 13, 19] {
389 assert!(ergodic_rayleigh(20, t, p).unwrap() * NATS < cmax);
390 }
391 for p_db in [0.0, 10.0, 20.0, 35.0] {
392 let p = comnum_math::db2lin(p_db);
393 let c = ergodic_rayleigh(20, 20, p).unwrap() / 20.0;
394 assert!((c / ergodic_rayleigh_limit(p) - 1.0).abs() < 5e-3, "{p_db} dB: {c}");
395 }
396 }
397
398 /// Telatar 1999, Fig. 3 (the $\varepsilon$-capacity of eq. (17) for $t = 1$): the
399 /// ends of the curves at $r = 10$ read on panel (c), $P = 30$ dB, 8.85 nats for
400 /// $\varepsilon = 10^{-1}$ and 7.75 nats for $10^{-4}$ (eq. (17) gives 8.74 and
401 /// 7.70); Fig. 4 (one receive antenna): at $P = 15$ dB the curves of
402 /// $t = 1, \dots, 10$ cross near $P(\Psi \le R) \approx 0.6$ at
403 /// $R \approx 3.5$ nats $= \ln(1 + P)$, more transmit antennas being better below the
404 /// crossing.
405 #[test]
406 fn telatar_1999_figs_3_4_outage() {
407 let p = 1e3;
408 let hi = outage_capacity_rayleigh(10, 1, p, 0.1).unwrap() * NATS;
409 let lo = outage_capacity_rayleigh(10, 1, p, 1e-4).unwrap() * NATS;
410 assert!((hi - 8.85).abs() < 0.15 && (lo - 7.75).abs() < 0.1, "{hi}, {lo}");
411 let p = comnum_math::db2lin(15.0);
412 let rate = p.ln_1p() / NATS;
413 let probs: Vec<f64> = (1..=10).map(|t| outage_rayleigh(1, t, p, rate).unwrap()).collect();
414 for w in probs.windows(2) {
415 assert!((w[1] - w[0]).abs() < 0.05, "{probs:?}");
416 }
417 assert!((probs[9] - 0.6).abs() < 0.1, "{probs:?}");
418 let low = 2.0 / NATS;
419 assert!(outage_rayleigh(1, 10, p, low).unwrap() < outage_rayleigh(1, 2, p, low).unwrap());
420 }
421}