Skip to content

Commit 62c4bc4

Browse files
committed
cmath module
1 parent 6642783 commit 62c4bc4

File tree

16 files changed

+1816
-172
lines changed

16 files changed

+1816
-172
lines changed

.github/workflows/rust.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Set up Python
2828
uses: actions/setup-python@v5
2929
with:
30-
python-version: "3.13"
30+
python-version: "3.14"
3131
check-latest: true
3232
- name: Setup Rust
3333
uses: actions-rs/toolchain@v1
@@ -39,10 +39,10 @@ jobs:
3939
run: cargo build --verbose
4040
- name: Run tests
4141
if: matrix.os != 'macos-latest'
42-
run: cargo test --verbose && cargo test --release --verbose
42+
run: cargo test --features complex --verbose && cargo test --features complex --release --verbose
4343
- name: Run tests with FMA (macOS)
4444
if: matrix.os == 'macos-latest'
45-
run: cargo test --verbose --features mul_add && cargo test --release --verbose --features mul_add
45+
run: cargo test --verbose --features complex,mul_add && cargo test --release --verbose --features complex,mul_add
4646
- name: Run tests with num-bigint
4747
run: cargo test --verbose --features num-bigint
4848
- name: Run tests with malachite-bigint

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.14.2

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ license = "PSF-2.0"
77

88

99
[features]
10-
default = ["mul_add"]
11-
# complex = ["dep:num-complex"]
10+
default = ["complex"]
11+
complex = ["dep:num-complex"]
1212
num-bigint = ["_bigint", "dep:num-bigint"]
1313
malachite-bigint = ["_bigint", "dep:malachite-bigint"]
1414
_bigint = ["dep:num-traits", "dep:num-integer"] # Internal feature. User must use num-bigint or malachite-bigint instead.

proptest-regressions/cmath.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc d9f364ff419553f1fcf92be2834534eb8bd5faacc285ed6587b5a3c827b235bd # shrinks to re = 1.00214445281558e32, im = -3.0523230669839245e-65
8+
cc 26832b65255697171026596e8fbeee37e5a2bf82133ca5cff83c21b95add285f # shrinks to re = 5.946781139174558e-217, im = 3.4143760786656616e281
9+
cc 78224345cd95a0451f2f83872fb7ca6f9462c7eed58bf5490d6b9b717c5b8e02 # shrinks to re = -9.234931944778561e90, im = 1.0662656145085839e88
10+
cc a1b5e651ca7e81b1cf0fee4c7fb4a982982d24a10b4d0b27ae38559b70f0e9db # shrinks to re = -0.6032998606032244, im = -4.778999871811813e-156

proptest-regressions/math.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Seeds for failure cases proptest has generated in the past. It is
2+
# automatically read and these particular cases re-run before any
3+
# novel cases are generated.
4+
#
5+
# It is recommended to check this file in to source control so that
6+
# everyone who runs the test benefits from these saved cases.
7+
cc 6f6becc96f663a83d20def559f516c7c5ce1a90b87c373d6c025dd3ab8f1fc39 # shrinks to x = 5.868849392888587e-309, y = 1.985586796867676e-308

src/cmath.rs

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

0 commit comments

Comments
 (0)