Skip to main content

comnum_math/
lib.rs

1//! # comnum-math — numerical methods without communication semantics
2//!
3//! Special functions, quadrature, FFT plans, nearest-point search and the split-step
4//! solver of the nonlinear Schrödinger equation. Nothing here knows what a symbol or a
5//! channel is: the crates above give these methods their meaning.
6//!
7//! # Contents
8//!
9//! | Module | Content |
10//! |---|---|
11//! | [`fft`] | cached FFT plans with the NumPy conventions, `fftfreq` |
12//! | [`geometry`] | vectors of the three-dimensional space: scalar product, norm, direction |
13//! | [`linalg`] | QR, Cholesky, Hermitian systems and the SVD of small complex matrices |
14//! | [`nearest`] | [`nearest::Slicer`], the nearest point of a finite set |
15//! | [`special`] | special functions: the error function, the Gaussian tail, Fresnel, Bessel, gamma, the exponential integral |
16//! | [`quadrature`] | Simpson's rule, the Gauss–Hermite and Gauss–Legendre rules |
17//! | [`log_domain`] | `ln(1 + e^x)`, `max*` and the log-sum-exp without overflow |
18//! | [`stats`] | the moments of order two and four of a complex sequence, the normalised correlation of two |
19//! | [`nlse`] | the symmetric split-step Fourier solvers of the scalar NLSE and of the Manakov equation |
20//! | [`gf2`] | polynomials and matrices over GF(2) |
21//! | [`gf2m`] | the Galois fields $\mathrm{GF}(2^{m})$ of the BCH and Reed–Solomon codes |
22//! | [`bigint`] | unsigned integers of arbitrary size, for enumerative coding |
23//! | [`bits`] | the Gray code [`gray`], [`gray_inverse`], and the conversions [`bits_to_index`], [`index_to_bits`] (most significant bit first) |
24//! | [`search`] | [`bisect`], [`golden_max`], [`argmax`] and [`gcd`] |
25//! | [`sequences`] | linear-feedback shift registers and the Gold sequence of 3GPP |
26//!
27//! At the root (the functions of [`special`], [`quadrature`] and [`log_domain`] are
28//! re-exported there):
29//!
30//! - the log domain: [`log_sum_exp`] (`ln Σ e^{v_i}`), [`ln_1p_exp`] (`ln(1 + e^x)`),
31//!   [`max_star`] (`ln(e^a + e^b)`), all without overflow;
32//! - [`C64`]: the complex type of every signal (`num_complex::Complex<f64>`), and
33//!   [`cis`], `e^{jθ}`;
34//! - decibels: [`db2lin`], [`lin2db`] (power ratios), [`db2amp`], [`amp2db`] (amplitude
35//!   ratios), [`dbm2w`], [`w2dbm`], [`db2np`], [`np2db`] (attenuation per unit length),
36//!   defined once in [`comnum_core::units`] next to the unit newtypes and re-exported here;
37//! - [`sinc`], `sin(πx)/(πx)`, [`diric`], the periodic sinc `sin(nx/2)/(n sin(x/2))`, and
38//!   [`effective_length`], `(1 - e^{-αL})/α`;
39//! - special functions: [`erf`], [`erfc`], [`erfcinv`], [`qfunc`] (the Gaussian tail
40//!   `Q(x) = ½ erfc(x/√2)`), its inverse [`qfuncinv`], [`fresnel`] (the Fresnel
41//!   integrals $C$ and $S$), [`bessel_j0`], [`bessel_i0e`], [`bessel_k`] and
42//!   [`ln_bessel_k`], [`ln_gamma`], [`gamma_p`] (the regularized incomplete gamma
43//!   function), [`expint_e1`] (the exponential integral);
44//! - [`entropy`], $-\sum_{i} p_{i} \log_{2} p_{i}$;
45//! - signal statistics: [`power`] (mean `|x|²`), [`energy`], [`papr_db`];
46//! - quadrature: [`simpson`], [`try_simpson`] and [`simpson_weight`] (composite
47//!   Simpson's rule), [`gauss_hermite`] (`∫ f(t) e^{-t²} dt`) and [`gauss_legendre`]
48//!   (`∫_{-1}^{1} f(t) dt`);
49//! - physical constants [`PLANCK`], [`BOLTZMANN`], [`ELEMENTARY_CHARGE`],
50//!   [`SPEED_OF_LIGHT`], and [`ZERO_CELSIUS`], the origin of the Celsius scale in kelvin.
51//!
52//! # Examples
53//! ```
54//! use comnum_math::*;
55//! assert!((db2lin(3.0) - 1.9953).abs() < 1e-4);
56//! assert!((dbm2w(0.0) - 1e-3).abs() < 1e-15);
57//! assert!((qfunc(0.0) - 0.5).abs() < 1e-7);
58//! let z = cis(std::f64::consts::FRAC_PI_2);
59//! assert!((z - C64::new(0.0, 1.0)).norm() < 1e-15);
60//! ```
61
62pub mod bigint;
63pub mod bits;
64pub mod fft;
65pub mod geometry;
66pub mod gf2;
67pub mod gf2m;
68pub mod linalg;
69pub mod log_domain;
70pub mod nearest;
71pub mod nlse;
72pub mod quadrature;
73pub mod search;
74pub mod sequences;
75pub mod special;
76pub mod stats;
77
78pub use bits::{bits_to_index, gray, gray_inverse, index_to_bits};
79pub use comnum_core::units::{amp2db, db2amp, db2lin, db2np, dbm2w, lin2db, np2db, w2dbm};
80pub use log_domain::{Jacobian, ln_1p_exp, log_sum_exp, log_sum_exp_iter, max_star};
81pub use quadrature::{
82    gauss_chebyshev, gauss_hermite, gauss_legendre, gaussian_nodes, simpson, simpson_weight, try_simpson,
83};
84pub use search::{argmax, bisect, gcd, golden_max};
85pub use special::{
86    bessel_i0e, bessel_j0, bessel_k, beta_inc, beta_inv, erf, erfc, erfcinv, expint_e1, fresnel, gamma_p, hermite_h,
87    hyp1f2, ln_bessel_k, ln_binomial_pmf, ln_gamma, normal_pdf, qfunc, qfuncinv,
88};
89
90pub use comnum_core::C64;
91
92/// Planck constant (J·s).
93pub const PLANCK: f64 = 6.626_070_15e-34;
94/// Boltzmann constant (J/K).
95pub const BOLTZMANN: f64 = 1.380_649e-23;
96/// Elementary charge (C).
97pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
98/// Speed of light in vacuum (m/s).
99pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
100/// The temperature of 0 °C (K): $T_{\mathrm{K}} = T_{\mathrm{°C}} + 273.15$.
101pub const ZERO_CELSIUS: f64 = 273.15;
102
103/// The floor of the scale (a norm or a span of the data) that relative tolerances are
104/// taken against, so that the tolerance of an all-zero input stays positive.
105pub(crate) const SCALE_FLOOR: f64 = 1e-300;
106
107/// `e^{jθ} = cos θ + j sin θ`.
108#[inline]
109pub fn cis(theta: f64) -> C64 {
110    let (s, c) = theta.sin_cos();
111    C64::new(c, s)
112}
113
114/// The normalised sinc `sin(πx)/(πx)`, 1 at 0.
115#[inline]
116pub fn sinc(x: f64) -> f64 {
117    if x == 0.0 { 1.0 } else { (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x) }
118}
119
120/// The effective length `(1 - e^{-αL})/α` of an attenuation `α` over `length` (`L`
121/// itself when `αL` is negligible).
122#[inline]
123pub fn effective_length(alpha: f64, length: f64) -> f64 {
124    if alpha * length < 1e-12 { length } else { (1.0 - (-alpha * length).exp()) / alpha }
125}
126
127/// The frequency $f = c / \lambda$ (Hz) of the vacuum wavelength `wavelength_nm` (nm), $c$
128/// the [`SPEED_OF_LIGHT`].
129///
130/// # Examples
131/// ```
132/// assert!((comnum_math::nm2hz(1550.0) - 193.414e12).abs() < 1e9);
133/// ```
134#[inline]
135pub fn nm2hz(wavelength_nm: f64) -> f64 {
136    SPEED_OF_LIGHT / (wavelength_nm * 1e-9)
137}
138
139/// Mean power `(1/N) Σ |x_n|²` of a complex signal (0 for an empty slice).
140pub fn power(x: &[C64]) -> f64 {
141    if x.is_empty() {
142        return 0.0;
143    }
144    energy(x) / x.len() as f64
145}
146
147/// Energy `Σ |x_n|²` of a complex signal.
148pub fn energy(x: &[C64]) -> f64 {
149    x.iter().map(C64::norm_sqr).sum()
150}
151
152/// The Dirichlet (periodic sinc) function of MATLAB, $\operatorname{diric}(x, n) =
153/// \frac{\sin(n x/2)}{n \sin(x/2)}$, of period $2\pi$ ($4\pi$) for odd (even) $n$, with its
154/// limits $(-1)^{k(n-1)}$ at $x = 2\pi k$; $\sin(n\theta)/\sin\theta = n
155/// \operatorname{diric}(2\theta, n)$ is the array factor of $n$ equally spaced terms.
156///
157/// # Examples
158/// ```
159/// use comnum_math::diric;
160/// assert_eq!(diric(0.0, 8), 1.0);
161/// assert_eq!(diric(2.0 * std::f64::consts::PI, 8), -1.0);
162/// let x = 0.3;
163/// assert!((diric(x, 5) - (2.5 * x).sin() / (5.0 * (0.5 * x).sin())).abs() < 1e-15);
164/// ```
165pub fn diric(x: f64, n: usize) -> f64 {
166    let half = 0.5 * x;
167    let den = half.sin();
168    let nf = n as f64;
169    if den.abs() < 1e-12 {
170        // x = 2πk: the limit (-1)^{k(n-1)}
171        let k = (half / std::f64::consts::PI).round() as i64;
172        return if (k * (n as i64 - 1)).rem_euclid(2) == 0 { 1.0 } else { -1.0 };
173    }
174    (nf * half).sin() / (nf * den)
175}
176
177/// Peak-to-average power ratio `max|x|² / mean|x|²` in dB (`-inf` for an empty or
178/// zero block).
179///
180/// # Examples
181/// ```
182/// use comnum_math::{papr_db, C64};
183/// let x = [C64::new(2.0, 0.0), C64::new(0.0, 0.0)];
184/// assert!((papr_db(&x) - 10.0 * 2f64.log10()).abs() < 1e-12);   // peak 4, mean 2
185/// ```
186pub fn papr_db(x: &[C64]) -> f64 {
187    let peak = x.iter().map(C64::norm_sqr).fold(0.0, f64::max);
188    let mean = power(x);
189    if mean > 0.0 { lin2db(peak / mean) } else { f64::NEG_INFINITY }
190}
191
192/// The entropy (bits) of a distribution, the zero probabilities left out:
193///
194/// $$
195/// H(p) = -\sum_{i:\, p_{i} > 0} p_{i} \log_{2} p_{i}.
196/// $$
197///
198/// # Examples
199/// ```
200/// use comnum_math::entropy;
201/// assert_eq!(entropy(&[0.25; 4]), 2.0);
202/// assert_eq!(entropy(&[1.0, 0.0]), 0.0);
203/// ```
204pub fn entropy(p: &[f64]) -> f64 {
205    p.iter().filter(|q| **q > 0.0).map(|q| -q * q.log2()).sum()
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn ln_bessel_k_returns_its_limits_instead_of_looping() {
214        assert_eq!(ln_bessel_k(0.0, f64::INFINITY), f64::NEG_INFINITY);
215        assert_eq!(ln_bessel_k(2.5, f64::INFINITY), f64::NEG_INFINITY);
216        assert_eq!(ln_bessel_k(f64::INFINITY, 1.0), f64::INFINITY);
217        assert_eq!(bessel_k(1.0, f64::INFINITY), 0.0);
218        assert!(ln_bessel_k(f64::NAN, 1.0).is_nan());
219        assert!(ln_bessel_k(1.0, f64::NAN).is_nan());
220        assert!(ln_bessel_k(1.0, 0.0).is_nan());
221        // ν/x overflows: the leading term ½Γ(ν)(2/x)^ν, which the quadrature matches
222        // just before the overflow
223        let lead = |nu: f64, x: f64| -std::f64::consts::LN_2 + ln_gamma(nu) + nu * (std::f64::consts::LN_2 - x.ln());
224        assert!((ln_bessel_k(2.0, 1e-300) / lead(2.0, 1e-300) - 1.0).abs() < 1e-12);
225        assert_eq!(ln_bessel_k(2.0, 1e-310), lead(2.0, 1e-310));
226        assert!(ln_bessel_k(0.01, 1e-320).is_nan());
227    }
228
229    #[test]
230    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
231    fn gamma_e1_and_erf_against_scipy() {
232        // scipy.special.gammaln, gammainc, exp1, erf (SciPy 1.18)
233        for (x, v) in
234            [(0.5, 0.5723649429247), (3.7, 1.428072326665388), (10.0, 12.801827480081469), (100.5, 361.43554046777757)]
235        {
236            assert!((ln_gamma(x) - v).abs() < 1e-13 * v.abs().max(1.0), "lgamma {x}");
237        }
238        assert!(ln_gamma(1.0).abs() < 1e-15 && ln_gamma(2.0).abs() < 1e-15);
239        for (a, x, v) in [
240            (2.5, 1.0, 0.15085496391539038),
241            (10.0, 6.2, 0.09837933549011063),
242            (10.0, 20.0, 0.9950045876916924),
243            (20.0, 5.0, 3.452135820914455e-7),
244            (0.5, 30.0, 0.9999999999999906),
245        ] {
246            assert!((gamma_p(a, x) / v - 1.0).abs() < 1e-12, "P({a}, {x}) = {} vs {v}", gamma_p(a, x));
247        }
248        for (x, v) in [
249            (1e-3, 6.331539364136149),
250            (0.3, 0.9056766516758468),
251            (2.5, 0.024914917870269736),
252            (50.0, 3.783264029550459e-24),
253        ] {
254            assert!((expint_e1(x) / v - 1.0).abs() < 1e-13, "E1({x})");
255        }
256        for (x, v) in
257            [(0.1, 0.1124629160182849), (1.9, 0.9927904292352575), (2.1, 0.997020533343667), (3.5, 0.9999992569016276)]
258        {
259            assert!((erf(x) - v).abs() < 1e-9, "erf({x})");
260            assert_eq!(erf(-x), -erf(x));
261        }
262    }
263
264    #[test]
265    fn bessel_i0e_matches_scipy() {
266        // scipy.special.i0e (SciPy 1.11), on both sides of the switch at 60
267        let table = [
268            (0.0, 1.0),
269            (0.5, 0.645_035_270_449_150_1),
270            (1.0, 0.465_759_607_593_640_43),
271            (5.0, 0.183_540_812_609_328_34),
272            (10.0, 0.127_833_337_163_428_6),
273            (30.0, 0.073_145_946_482_237_3),
274            (59.9, 0.051_654_795_368_473_484),
275            (60.1, 0.051_568_411_420_294_53),
276            (100.0, 0.039_944_379_299_096_68),
277            (1e3, 0.012_617_240_455_891_257),
278            (1e5, 0.001_261_567_837_976_776_6),
279        ];
280        for (x, want) in table {
281            let got = bessel_i0e(x);
282            assert!((got / want - 1.0).abs() < 1e-13, "{x}: {got} vs {want}");
283            assert_eq!(bessel_i0e(-x), got);
284        }
285    }
286
287    #[test]
288    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
289    fn bessel_j0_against_scipy() {
290        // scipy.special.j0
291        for (x, v) in [
292            (0.0, 1.0),
293            (0.0628, 0.9990142830026578),
294            (1.0, 0.7651976865579665),
295            (2.404825557695773, 0.0),
296            (5.0, -0.1775967713143383),
297            (10.0, -0.2459357644513483),
298            (30.0, -0.08636798358104023),
299            (-3.0, -0.2600519549019335),
300        ] {
301            assert!((bessel_j0(x) - v).abs() < 1e-14, "J0({x}) = {} vs {v}", bessel_j0(x));
302        }
303    }
304
305    #[test]
306    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
307    fn fresnel_against_scipy() {
308        // scipy.special.fresnel 1.x (which returns (S, C)): (x, C, S)
309        let table = [
310            (0.1, 0.09999753262708506, 0.0005235895476122108),
311            (0.5, 0.4923442258714464, 0.06473243285999929),
312            (1.0, 0.779893400376823, 0.4382591473903547),
313            (1.4999, 0.4453535730062187, 0.6975432066559986),
314            (1.5, 0.44526117603982146, 0.697504960082093),
315            (2.0, 0.48825340607534073, 0.34341567836369824),
316            (3.7, 0.5419456621544874, 0.5749803498874729),
317            (10.0, 0.49989869420551575, 0.46816997858488224),
318            (31.4, 0.5006397897427959, 0.51011704529959),
319            (-2.5, -0.45741300964177706, -0.6191817558195929),
320        ];
321        for (x, c, s) in table {
322            let (fc, fs) = fresnel(x);
323            assert!((fc - c).abs() < 1e-14 && (fs - s).abs() < 1e-14, "{x}: ({fc}, {fs}) vs ({c}, {s})");
324        }
325        assert_eq!(fresnel(0.0), (0.0, 0.0));
326        // C, S → 1/2 at infinity
327        let (c, s) = fresnel(1e4);
328        assert!((c - 0.5).abs() < 1e-4 && (s - 0.5).abs() < 1e-4);
329    }
330
331    #[test]
332    #[allow(clippy::unreadable_literal, clippy::excessive_precision, reason = "the values of mpmath, digit for digit")]
333    fn erfc_reference_values() {
334        // mpmath 1.3 with 40 digits, rounded to 20: every branch of Cody's approximations
335        // (|x| ≤ 0.46875, up to 4, beyond) and the tail down to 5.7·10⁻²⁹⁶
336        for (x, v) in [
337            (0.0, 1.0),
338            (1e-10, 0.99999999988716208329),
339            (0.1, 0.8875370839817151078),
340            (0.3, 0.67137324054087257236),
341            (0.46875, 0.50738652678206200841),
342            (0.5, 0.47950012218695346232),
343            (0.75, 0.2888443663464848684),
344            (1.0, 0.15729920705028513066),
345            (1.5, 0.033894853524689272933),
346            (2.0, 0.0046777349810472658379),
347            (3.0, 0.000022090496998585441373),
348            (4.0, 1.5417257900280018852e-8),
349            (4.5, 1.9661604415428874763e-10),
350            (5.0, 1.5374597944280348502e-12),
351            (6.0, 2.1519736712498913117e-17),
352            (8.0, 1.122429717298292708e-29),
353            (10.0, 2.088487583762544757e-45),
354            (15.0, 7.2129941724512066666e-100),
355            (20.0, 5.3958656116079009289e-176),
356            (26.0, 5.6631924088561428465e-296),
357        ] {
358            assert!((erfc(x) / v - 1.0).abs() < 1e-15, "erfc({x}) = {} against {v}", erfc(x));
359            assert!((erfc(-x) - (2.0 - v)).abs() < 1e-15, "erfc(-{x})");
360        }
361        for (x, v) in [(-0.3, 1.3286267594591274276), (-1.0, 1.8427007929497148693), (-3.0, 1.9999779095030014146)] {
362            assert!((erfc(x) / v - 1.0).abs() < 1e-15, "erfc({x})");
363        }
364        // the subnormal range, then zero, the limits and NaN
365        assert!((erfc(27.0) / 5.237048923789255685e-319 - 1.0).abs() < 1e-3);
366        assert_eq!((erfc(27.5), erfc(f64::INFINITY), erfc(f64::NEG_INFINITY)), (0.0, 0.0, 2.0));
367        assert!(erfc(f64::NAN).is_nan());
368        assert!((erfcinv(erfc(1.3)) - 1.3).abs() < 1e-9);
369        // Q(4.753424308822899) = 1.0000000000000013·10⁻⁶ (mpmath), within the rounding of
370        // x/√2 amplified by x² ≈ 23
371        assert!((qfunc(4.753_424_308_822_899) / 1.000_000_000_000_001_3e-6 - 1.0).abs() < 1e-14);
372    }
373
374    #[test]
375    fn decibels_round_trip() {
376        assert!((lin2db(db2lin(7.3)) - 7.3).abs() < 1e-12);
377        assert!((w2dbm(dbm2w(-3.0)) + 3.0).abs() < 1e-12);
378    }
379}