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::{gauss_hermite, gauss_legendre, gaussian_nodes, simpson, simpson_weight, try_simpson};
82pub use search::{argmax, bisect, gcd, golden_max};
83pub use special::{
84    bessel_i0e, bessel_j0, bessel_k, beta_inc, beta_inv, erf, erfc, erfcinv, expint_e1, fresnel, gamma_p, ln_bessel_k,
85    ln_binomial_pmf, ln_gamma, normal_pdf, qfunc, qfuncinv,
86};
87
88pub use comnum_core::C64;
89
90/// Planck constant (J·s).
91pub const PLANCK: f64 = 6.626_070_15e-34;
92/// Boltzmann constant (J/K).
93pub const BOLTZMANN: f64 = 1.380_649e-23;
94/// Elementary charge (C).
95pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
96/// Speed of light in vacuum (m/s).
97pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
98/// The temperature of 0 °C (K): $T_{\mathrm{K}} = T_{\mathrm{°C}} + 273.15$.
99pub const ZERO_CELSIUS: f64 = 273.15;
100
101/// The floor of the scale (a norm or a span of the data) that relative tolerances are
102/// taken against, so that the tolerance of an all-zero input stays positive.
103pub(crate) const SCALE_FLOOR: f64 = 1e-300;
104
105/// `e^{jθ} = cos θ + j sin θ`.
106#[inline]
107pub fn cis(theta: f64) -> C64 {
108    let (s, c) = theta.sin_cos();
109    C64::new(c, s)
110}
111
112/// The normalised sinc `sin(πx)/(πx)`, 1 at 0.
113#[inline]
114pub fn sinc(x: f64) -> f64 {
115    if x == 0.0 { 1.0 } else { (std::f64::consts::PI * x).sin() / (std::f64::consts::PI * x) }
116}
117
118/// The effective length `(1 - e^{-αL})/α` of an attenuation `α` over `length` (`L`
119/// itself when `αL` is negligible).
120#[inline]
121pub fn effective_length(alpha: f64, length: f64) -> f64 {
122    if alpha * length < 1e-12 { length } else { (1.0 - (-alpha * length).exp()) / alpha }
123}
124
125/// The frequency $f = c / \lambda$ (Hz) of the vacuum wavelength `wavelength_nm` (nm), $c$
126/// the [`SPEED_OF_LIGHT`].
127///
128/// # Examples
129/// ```
130/// assert!((comnum_math::nm2hz(1550.0) - 193.414e12).abs() < 1e9);
131/// ```
132#[inline]
133pub fn nm2hz(wavelength_nm: f64) -> f64 {
134    SPEED_OF_LIGHT / (wavelength_nm * 1e-9)
135}
136
137/// Mean power `(1/N) Σ |x_n|²` of a complex signal (0 for an empty slice).
138pub fn power(x: &[C64]) -> f64 {
139    if x.is_empty() {
140        return 0.0;
141    }
142    energy(x) / x.len() as f64
143}
144
145/// Energy `Σ |x_n|²` of a complex signal.
146pub fn energy(x: &[C64]) -> f64 {
147    x.iter().map(C64::norm_sqr).sum()
148}
149
150/// The Dirichlet (periodic sinc) function of MATLAB, $\operatorname{diric}(x, n) =
151/// \frac{\sin(n x/2)}{n \sin(x/2)}$, of period $2\pi$ ($4\pi$) for odd (even) $n$, with its
152/// limits $(-1)^{k(n-1)}$ at $x = 2\pi k$; $\sin(n\theta)/\sin\theta = n
153/// \operatorname{diric}(2\theta, n)$ is the array factor of $n$ equally spaced terms.
154///
155/// # Examples
156/// ```
157/// use comnum_math::diric;
158/// assert_eq!(diric(0.0, 8), 1.0);
159/// assert_eq!(diric(2.0 * std::f64::consts::PI, 8), -1.0);
160/// let x = 0.3;
161/// assert!((diric(x, 5) - (2.5 * x).sin() / (5.0 * (0.5 * x).sin())).abs() < 1e-15);
162/// ```
163pub fn diric(x: f64, n: usize) -> f64 {
164    let half = 0.5 * x;
165    let den = half.sin();
166    let nf = n as f64;
167    if den.abs() < 1e-12 {
168        // x = 2πk: the limit (-1)^{k(n-1)}
169        let k = (half / std::f64::consts::PI).round() as i64;
170        return if (k * (n as i64 - 1)).rem_euclid(2) == 0 { 1.0 } else { -1.0 };
171    }
172    (nf * half).sin() / (nf * den)
173}
174
175/// Peak-to-average power ratio `max|x|² / mean|x|²` in dB (`-inf` for an empty or
176/// zero block).
177///
178/// # Examples
179/// ```
180/// use comnum_math::{papr_db, C64};
181/// let x = [C64::new(2.0, 0.0), C64::new(0.0, 0.0)];
182/// assert!((papr_db(&x) - 10.0 * 2f64.log10()).abs() < 1e-12);   // peak 4, mean 2
183/// ```
184pub fn papr_db(x: &[C64]) -> f64 {
185    let peak = x.iter().map(C64::norm_sqr).fold(0.0, f64::max);
186    let mean = power(x);
187    if mean > 0.0 { lin2db(peak / mean) } else { f64::NEG_INFINITY }
188}
189
190/// The entropy (bits) of a distribution, the zero probabilities left out:
191///
192/// $$
193/// H(p) = -\sum_{i:\, p_{i} > 0} p_{i} \log_{2} p_{i}.
194/// $$
195///
196/// # Examples
197/// ```
198/// use comnum_math::entropy;
199/// assert_eq!(entropy(&[0.25; 4]), 2.0);
200/// assert_eq!(entropy(&[1.0, 0.0]), 0.0);
201/// ```
202pub fn entropy(p: &[f64]) -> f64 {
203    p.iter().filter(|q| **q > 0.0).map(|q| -q * q.log2()).sum()
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn ln_bessel_k_returns_its_limits_instead_of_looping() {
212        assert_eq!(ln_bessel_k(0.0, f64::INFINITY), f64::NEG_INFINITY);
213        assert_eq!(ln_bessel_k(2.5, f64::INFINITY), f64::NEG_INFINITY);
214        assert_eq!(ln_bessel_k(f64::INFINITY, 1.0), f64::INFINITY);
215        assert_eq!(bessel_k(1.0, f64::INFINITY), 0.0);
216        assert!(ln_bessel_k(f64::NAN, 1.0).is_nan());
217        assert!(ln_bessel_k(1.0, f64::NAN).is_nan());
218        assert!(ln_bessel_k(1.0, 0.0).is_nan());
219        // ν/x overflows: the leading term ½Γ(ν)(2/x)^ν, which the quadrature matches
220        // just before the overflow
221        let lead = |nu: f64, x: f64| -std::f64::consts::LN_2 + ln_gamma(nu) + nu * (std::f64::consts::LN_2 - x.ln());
222        assert!((ln_bessel_k(2.0, 1e-300) / lead(2.0, 1e-300) - 1.0).abs() < 1e-12);
223        assert_eq!(ln_bessel_k(2.0, 1e-310), lead(2.0, 1e-310));
224        assert!(ln_bessel_k(0.01, 1e-320).is_nan());
225    }
226
227    #[test]
228    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
229    fn gamma_e1_and_erf_against_scipy() {
230        // scipy.special.gammaln, gammainc, exp1, erf (SciPy 1.18)
231        for (x, v) in
232            [(0.5, 0.5723649429247), (3.7, 1.428072326665388), (10.0, 12.801827480081469), (100.5, 361.43554046777757)]
233        {
234            assert!((ln_gamma(x) - v).abs() < 1e-13 * v.abs().max(1.0), "lgamma {x}");
235        }
236        assert!(ln_gamma(1.0).abs() < 1e-15 && ln_gamma(2.0).abs() < 1e-15);
237        for (a, x, v) in [
238            (2.5, 1.0, 0.15085496391539038),
239            (10.0, 6.2, 0.09837933549011063),
240            (10.0, 20.0, 0.9950045876916924),
241            (20.0, 5.0, 3.452135820914455e-7),
242            (0.5, 30.0, 0.9999999999999906),
243        ] {
244            assert!((gamma_p(a, x) / v - 1.0).abs() < 1e-12, "P({a}, {x}) = {} vs {v}", gamma_p(a, x));
245        }
246        for (x, v) in [
247            (1e-3, 6.331539364136149),
248            (0.3, 0.9056766516758468),
249            (2.5, 0.024914917870269736),
250            (50.0, 3.783264029550459e-24),
251        ] {
252            assert!((expint_e1(x) / v - 1.0).abs() < 1e-13, "E1({x})");
253        }
254        for (x, v) in
255            [(0.1, 0.1124629160182849), (1.9, 0.9927904292352575), (2.1, 0.997020533343667), (3.5, 0.9999992569016276)]
256        {
257            assert!((erf(x) - v).abs() < 1e-9, "erf({x})");
258            assert_eq!(erf(-x), -erf(x));
259        }
260    }
261
262    #[test]
263    fn bessel_i0e_matches_scipy() {
264        // scipy.special.i0e (SciPy 1.11), on both sides of the switch at 60
265        let table = [
266            (0.0, 1.0),
267            (0.5, 0.645_035_270_449_150_1),
268            (1.0, 0.465_759_607_593_640_43),
269            (5.0, 0.183_540_812_609_328_34),
270            (10.0, 0.127_833_337_163_428_6),
271            (30.0, 0.073_145_946_482_237_3),
272            (59.9, 0.051_654_795_368_473_484),
273            (60.1, 0.051_568_411_420_294_53),
274            (100.0, 0.039_944_379_299_096_68),
275            (1e3, 0.012_617_240_455_891_257),
276            (1e5, 0.001_261_567_837_976_776_6),
277        ];
278        for (x, want) in table {
279            let got = bessel_i0e(x);
280            assert!((got / want - 1.0).abs() < 1e-13, "{x}: {got} vs {want}");
281            assert_eq!(bessel_i0e(-x), got);
282        }
283    }
284
285    #[test]
286    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
287    fn bessel_j0_against_scipy() {
288        // scipy.special.j0
289        for (x, v) in [
290            (0.0, 1.0),
291            (0.0628, 0.9990142830026578),
292            (1.0, 0.7651976865579665),
293            (2.404825557695773, 0.0),
294            (5.0, -0.1775967713143383),
295            (10.0, -0.2459357644513483),
296            (30.0, -0.08636798358104023),
297            (-3.0, -0.2600519549019335),
298        ] {
299            assert!((bessel_j0(x) - v).abs() < 1e-14, "J0({x}) = {} vs {v}", bessel_j0(x));
300        }
301    }
302
303    #[test]
304    #[allow(clippy::unreadable_literal, reason = "the values of SciPy, digit for digit")]
305    fn fresnel_against_scipy() {
306        // scipy.special.fresnel 1.x (which returns (S, C)): (x, C, S)
307        let table = [
308            (0.1, 0.09999753262708506, 0.0005235895476122108),
309            (0.5, 0.4923442258714464, 0.06473243285999929),
310            (1.0, 0.779893400376823, 0.4382591473903547),
311            (1.4999, 0.4453535730062187, 0.6975432066559986),
312            (1.5, 0.44526117603982146, 0.697504960082093),
313            (2.0, 0.48825340607534073, 0.34341567836369824),
314            (3.7, 0.5419456621544874, 0.5749803498874729),
315            (10.0, 0.49989869420551575, 0.46816997858488224),
316            (31.4, 0.5006397897427959, 0.51011704529959),
317            (-2.5, -0.45741300964177706, -0.6191817558195929),
318        ];
319        for (x, c, s) in table {
320            let (fc, fs) = fresnel(x);
321            assert!((fc - c).abs() < 1e-14 && (fs - s).abs() < 1e-14, "{x}: ({fc}, {fs}) vs ({c}, {s})");
322        }
323        assert_eq!(fresnel(0.0), (0.0, 0.0));
324        // C, S → 1/2 at infinity
325        let (c, s) = fresnel(1e4);
326        assert!((c - 0.5).abs() < 1e-4 && (s - 0.5).abs() < 1e-4);
327    }
328
329    #[test]
330    #[allow(clippy::unreadable_literal, clippy::excessive_precision, reason = "the values of mpmath, digit for digit")]
331    fn erfc_reference_values() {
332        // mpmath 1.3 with 40 digits, rounded to 20: every branch of Cody's approximations
333        // (|x| ≤ 0.46875, up to 4, beyond) and the tail down to 5.7·10⁻²⁹⁶
334        for (x, v) in [
335            (0.0, 1.0),
336            (1e-10, 0.99999999988716208329),
337            (0.1, 0.8875370839817151078),
338            (0.3, 0.67137324054087257236),
339            (0.46875, 0.50738652678206200841),
340            (0.5, 0.47950012218695346232),
341            (0.75, 0.2888443663464848684),
342            (1.0, 0.15729920705028513066),
343            (1.5, 0.033894853524689272933),
344            (2.0, 0.0046777349810472658379),
345            (3.0, 0.000022090496998585441373),
346            (4.0, 1.5417257900280018852e-8),
347            (4.5, 1.9661604415428874763e-10),
348            (5.0, 1.5374597944280348502e-12),
349            (6.0, 2.1519736712498913117e-17),
350            (8.0, 1.122429717298292708e-29),
351            (10.0, 2.088487583762544757e-45),
352            (15.0, 7.2129941724512066666e-100),
353            (20.0, 5.3958656116079009289e-176),
354            (26.0, 5.6631924088561428465e-296),
355        ] {
356            assert!((erfc(x) / v - 1.0).abs() < 1e-15, "erfc({x}) = {} against {v}", erfc(x));
357            assert!((erfc(-x) - (2.0 - v)).abs() < 1e-15, "erfc(-{x})");
358        }
359        for (x, v) in [(-0.3, 1.3286267594591274276), (-1.0, 1.8427007929497148693), (-3.0, 1.9999779095030014146)] {
360            assert!((erfc(x) / v - 1.0).abs() < 1e-15, "erfc({x})");
361        }
362        // the subnormal range, then zero, the limits and NaN
363        assert!((erfc(27.0) / 5.237048923789255685e-319 - 1.0).abs() < 1e-3);
364        assert_eq!((erfc(27.5), erfc(f64::INFINITY), erfc(f64::NEG_INFINITY)), (0.0, 0.0, 2.0));
365        assert!(erfc(f64::NAN).is_nan());
366        assert!((erfcinv(erfc(1.3)) - 1.3).abs() < 1e-9);
367        // Q(4.753424308822899) = 1.0000000000000013·10⁻⁶ (mpmath), within the rounding of
368        // x/√2 amplified by x² ≈ 23
369        assert!((qfunc(4.753_424_308_822_899) / 1.000_000_000_000_001_3e-6 - 1.0).abs() < 1e-14);
370    }
371
372    #[test]
373    fn decibels_round_trip() {
374        assert!((lin2db(db2lin(7.3)) - 7.3).abs() < 1e-12);
375        assert!((w2dbm(dbm2w(-3.0)) + 3.0).abs() < 1e-12);
376    }
377}