Skip to main content

pymath/
cmath.rs

1//! Complex math functions matching Python's cmath module behavior.
2//!
3//! These implementations follow the algorithms from cmathmodule.c
4//! to ensure numerical precision and correct handling of edge cases.
5
6mod exponential;
7mod misc;
8mod trigonometric;
9
10pub use exponential::{exp, log, log10, sqrt};
11pub use misc::{abs, isclose, isfinite, isinf, isnan, phase, polar, rect};
12pub use trigonometric::{acos, acosh, asin, asinh, atan, atanh, cos, cosh, sin, sinh, tan, tanh};
13
14use num_complex::Complex64;
15
16// Public constants (matching Python's cmath module)
17
18/// The mathematical constant e = 2.718281...
19pub const E: f64 = std::f64::consts::E;
20
21/// The mathematical constant π = 3.141592...
22pub const PI: f64 = std::f64::consts::PI;
23
24/// The mathematical constant τ = 6.283185...
25pub const TAU: f64 = std::f64::consts::TAU;
26
27/// Positive infinity.
28pub const INF: f64 = f64::INFINITY;
29
30/// A floating point "not a number" (NaN) value.
31pub const NAN: f64 = f64::NAN;
32
33/// Complex number with zero real part and positive infinity imaginary part.
34pub const INFJ: Complex64 = Complex64::new(0.0, f64::INFINITY);
35
36/// Complex number with zero real part and NaN imaginary part.
37pub const NANJ: Complex64 = Complex64::new(0.0, f64::NAN);
38
39#[cfg(test)]
40use crate::Result;
41use crate::m;
42
43// Shared constants
44
45const M_LN2: f64 = core::f64::consts::LN_2;
46
47/// Used to avoid spurious overflow in sqrt, log, inverse trig/hyperbolic functions.
48const CM_LARGE_DOUBLE: f64 = f64::MAX / 4.0;
49const CM_LOG_LARGE_DOUBLE: f64 = 709.0895657128241; // log(CM_LARGE_DOUBLE)
50
51// Special value table constants
52const P: f64 = core::f64::consts::PI;
53const P14: f64 = 0.25 * core::f64::consts::PI;
54const P12: f64 = 0.5 * core::f64::consts::PI;
55const P34: f64 = 0.75 * core::f64::consts::PI;
56const N: f64 = f64::NAN;
57#[allow(clippy::excessive_precision)]
58const U: f64 = -9.5426319407711027e33; // unlikely value, used as placeholder
59
60/// Helper to create Complex64 in const context (for special value tables)
61#[inline]
62const fn c(re: f64, im: f64) -> num_complex::Complex64 {
63    num_complex::Complex64::new(re, im)
64}
65
66/// Special value types for classifying doubles.
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68#[repr(usize)]
69enum SpecialType {
70    NInf = 0,  // negative infinity
71    Neg = 1,   // negative finite (nonzero)
72    NZero = 2, // -0.
73    PZero = 3, // +0.
74    Pos = 4,   // positive finite (nonzero)
75    PInf = 5,  // positive infinity
76    Nan = 6,   // NaN
77}
78
79/// Return special value from table if input is non-finite.
80macro_rules! special_value {
81    ($z:expr, $table:expr) => {
82        if !$z.re.is_finite() || !$z.im.is_finite() {
83            return Ok($table[special_type($z.re) as usize][special_type($z.im) as usize]);
84        }
85    };
86}
87pub(crate) use special_value;
88
89/// Classify a double into one of seven special types.
90#[inline]
91fn special_type(d: f64) -> SpecialType {
92    if d.is_finite() {
93        if d != 0.0 {
94            if m::copysign(1.0, d) == 1.0 {
95                SpecialType::Pos
96            } else {
97                SpecialType::Neg
98            }
99        } else if m::copysign(1.0, d) == 1.0 {
100            SpecialType::PZero
101        } else {
102            SpecialType::NZero
103        }
104    } else if d.is_nan() {
105        SpecialType::Nan
106    } else if m::copysign(1.0, d) == 1.0 {
107        SpecialType::PInf
108    } else {
109        SpecialType::NInf
110    }
111}
112
113#[cfg(test)]
114pub(crate) mod tests {
115    use super::*;
116
117    /// Compare complex result with CPython, allowing small ULP differences for finite values.
118    pub fn assert_complex_eq(py_re: f64, py_im: f64, rs: Complex64, func: &str, re: f64, im: f64) {
119        let check_component = |py: f64, rs: f64, component: &str| {
120            if py.is_nan() && rs.is_nan() {
121                // Both NaN - OK
122            } else if py.is_nan() || rs.is_nan() {
123                panic!("{func}({re}, {im}).{component}: py={py} vs rs={rs} (one is NaN)",);
124            } else if py.is_infinite() && rs.is_infinite() {
125                // Check sign matches
126                if py.is_sign_positive() != rs.is_sign_positive() {
127                    panic!("{func}({re}, {im}).{component}: py={py} vs rs={rs} (sign mismatch)",);
128                }
129            } else if py.is_infinite() || rs.is_infinite() {
130                panic!("{func}({re}, {im}).{component}: py={py} vs rs={rs} (one is infinite)",);
131            } else {
132                // Both finite - allow small ULP difference
133                let py_bits = py.to_bits() as i64;
134                let rs_bits = rs.to_bits() as i64;
135                let ulp_diff = (py_bits - rs_bits).abs();
136                if ulp_diff != 0 {
137                    panic!(
138                        "{func}({re}, {im}).{component}: py={py} (bits={:#x}) vs rs={rs} (bits={:#x}), ULP diff={ulp_diff}",
139                        py.to_bits(),
140                        rs.to_bits()
141                    );
142                }
143            }
144        };
145        check_component(py_re, rs.re, "re");
146        check_component(py_im, rs.im, "im");
147    }
148
149    pub fn test_cmath_func<F>(func_name: &str, rs_func: F, re: f64, im: f64)
150    where
151        F: Fn(Complex64) -> Result<Complex64>,
152    {
153        use pyo3::prelude::*;
154
155        let rs_result = rs_func(Complex64::new(re, im));
156
157        pyo3::Python::attach(|py| {
158            let cmath = pyo3::types::PyModule::import(py, "cmath").unwrap();
159            let py_func = cmath.getattr(func_name).unwrap();
160            let py_result = py_func.call1((pyo3::types::PyComplex::from_doubles(py, re, im),));
161
162            match py_result {
163                Ok(result) => {
164                    use pyo3::types::PyComplexMethods;
165                    let c = result.cast::<pyo3::types::PyComplex>().unwrap();
166                    let py_re = c.real();
167                    let py_im = c.imag();
168                    match rs_result {
169                        Ok(rs) => {
170                            assert_complex_eq(py_re, py_im, rs, func_name, re, im);
171                        }
172                        Err(e) => {
173                            panic!(
174                                "{func_name}({re}, {im}): py=({py_re}, {py_im}) but rs returned error {e:?}"
175                            );
176                        }
177                    }
178                }
179                Err(e) => {
180                    // CPython raised an exception - check we got an error too
181                    if let Ok(rs) = rs_result {
182                        // Some special cases may return values for domain errors in Python
183                        // Check if it's a domain error
184                        if e.is_instance_of::<pyo3::exceptions::PyValueError>(py) {
185                            panic!(
186                                "{func_name}({re}, {im}): py raised ValueError but rs=({}, {})",
187                                rs.re, rs.im
188                            );
189                        } else if e.is_instance_of::<pyo3::exceptions::PyOverflowError>(py) {
190                            panic!(
191                                "{func_name}({re}, {im}): py raised OverflowError but rs=({}, {})",
192                                rs.re, rs.im
193                            );
194                        }
195                    }
196                    // Both raised errors - OK
197                }
198            }
199        });
200    }
201}