Skip to content

Commit f95c66d

Browse files
committed
fix a lot
1 parent 3eb634e commit f95c66d

File tree

11 files changed

+607
-239
lines changed

11 files changed

+607
-239
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "pymath"
33
author = "Jeong, YunWon <[email protected]>"
44
repository = "https://github.com/RustPython/pymath"
55
description = "A binary representation compatible Rust implementation of Python's math library."
6-
version = "0.1.1"
6+
version = "0.1.2"
77
edition = "2024"
88
license = "PSF-2.0"
99

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ Every function produces identical results to Python at the binary representation
5757
- [x] `nextafter`
5858
- [x] `pi`
5959
- [x] `pow`
60-
- [x] `prod`
60+
- [x] `prod` (`prod`, `prod_int`)
6161
- [x] `radians`
6262
- [x] `remainder`
6363
- [x] `sin`
6464
- [x] `sinh`
6565
- [x] `sqrt`
66-
- [x] `sumprod` (`sumprod`, `sumprod_int`, `sumprod_generic`)
66+
- [x] `sumprod` (`sumprod`, `sumprod_int`)
6767
- [x] `tan`
6868
- [x] `tanh`
6969
- [x] `tau`

proptest-regressions/cmath/trigonometric.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ cc 409f359905a7e77eacdbd4643c3a1483cc128e46a1c06795c246f3d57a2e61b9 # shrinks to
99
cc 5acbcf76c3bcbaf0b2b89a987e38c75265434f96fdc11598830e2221df72fc53 # shrinks to re = 0.00010260965539526095, im = 4.984721877290597e-19
1010
cc ac04191f916633de0408e071f5f6ada8f7fe7a1be02caca7a32f37ecb148a5ac # shrinks to re = 3.049837651806167e74, im = -2.222842222335753e-166
1111
cc 77ec3c08d2e057fb9467eb13c3b953e4e30f2800259d3653f7bcdfcbaf53614f # shrinks to re = 7.812038268590211e52, im = -2.623972069152808e-109
12+
cc c8fa2873ca664202a38d40ef3766b0f9f07dc82c1d5cd08dcd6d180564a013a1 # shrinks to re = -0.0016106222874309162, im = 0.10954881424244108

proptest-regressions/math/misc.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 46166a18f59569bb11107fd280e53c8f37902760e15afbe67d6951dcd3032c72 # shrinks to x = 0.0, y = 0.0, z = 0.0

src/cmath/exponential.rs

Lines changed: 155 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ pub fn exp(z: Complex64) -> Result<Complex64> {
131131
Ok(Complex64::new(r_re, r_im))
132132
}
133133

134-
/// Complex natural logarithm.
134+
/// Complex natural logarithm (ln, base e).
135+
/// TODO: consider to expose API
135136
#[inline]
136-
pub fn log(z: Complex64) -> Result<Complex64> {
137+
pub(crate) fn ln(z: Complex64) -> Result<Complex64> {
137138
special_value!(z, LOG_SPECIAL_VALUES);
138139

139140
let ax = m::fabs(z.re);
@@ -149,8 +150,10 @@ pub fn log(z: Complex64) -> Result<Complex64> {
149150
m::ldexp(ay, f64::MANTISSA_DIGITS as i32),
150151
)) - f64::MANTISSA_DIGITS as f64 * M_LN2
151152
} else {
152-
// log(+/-0. +/- 0i)
153-
return Err(Error::EDOM);
153+
// log(+/-0. +/- 0i) - return -inf like CPython does
154+
// Note: CPython sets errno=EDOM but still returns a value.
155+
// When used with a base, the second c_log call clears errno.
156+
f64::NEG_INFINITY
154157
}
155158
} else {
156159
let h = m::hypot(ax, ay);
@@ -167,10 +170,90 @@ pub fn log(z: Complex64) -> Result<Complex64> {
167170
Ok(Complex64::new(r_re, r_im))
168171
}
169172

173+
/// Complex logarithm with optional base.
174+
///
175+
/// If base is None, returns the natural logarithm.
176+
/// If base is Some(b), returns log(z) / log(b).
177+
#[inline]
178+
pub fn log(z: Complex64, base: Option<Complex64>) -> Result<Complex64> {
179+
// c_log always returns a value, but sets errno for special cases.
180+
// The error check happens at the end of cmath_log_impl.
181+
// For log(z) without base: z=0 raises EDOM
182+
// For log(z, base): z=0 doesn't raise because c_log(base) clears errno
183+
let z_is_zero = z.re == 0.0 && z.im == 0.0;
184+
match base {
185+
None => {
186+
// No base: raise error if z=0
187+
if z_is_zero {
188+
return Err(Error::EDOM);
189+
}
190+
ln(z)
191+
}
192+
Some(b) => {
193+
// With base: z=0 is allowed (second ln clears the "errno")
194+
let log_z = ln(z)?;
195+
let log_b = ln(b)?;
196+
// Use _Py_c_quot-style division to preserve sign of zero
197+
Ok(c_quot(log_z, log_b))
198+
}
199+
}
200+
}
201+
202+
/// Complex division following _Py_c_quot algorithm.
203+
/// This preserves the sign of zero correctly and recovers infinities
204+
/// from NaN results per C11 Annex G.5.2.
205+
#[inline]
206+
fn c_quot(a: Complex64, b: Complex64) -> Complex64 {
207+
let abs_breal = m::fabs(b.re);
208+
let abs_bimag = m::fabs(b.im);
209+
210+
let mut r = if abs_breal >= abs_bimag {
211+
if abs_breal == 0.0 {
212+
Complex64::new(f64::NAN, f64::NAN)
213+
} else {
214+
let ratio = b.im / b.re;
215+
let denom = b.re + b.im * ratio;
216+
Complex64::new((a.re + a.im * ratio) / denom, (a.im - a.re * ratio) / denom)
217+
}
218+
} else if abs_bimag >= abs_breal {
219+
let ratio = b.re / b.im;
220+
let denom = b.re * ratio + b.im;
221+
Complex64::new((a.re * ratio + a.im) / denom, (a.im * ratio - a.re) / denom)
222+
} else {
223+
// At least one of b.re or b.im is NaN
224+
Complex64::new(f64::NAN, f64::NAN)
225+
};
226+
227+
// Recover infinities and zeros that computed as nan+nanj.
228+
// See C11 Annex G.5.2, routine _Cdivd().
229+
if r.re.is_nan() && r.im.is_nan() {
230+
if (a.re.is_infinite() || a.im.is_infinite()) && b.re.is_finite() && b.im.is_finite() {
231+
let x = m::copysign(if a.re.is_infinite() { 1.0 } else { 0.0 }, a.re);
232+
let y = m::copysign(if a.im.is_infinite() { 1.0 } else { 0.0 }, a.im);
233+
r.re = f64::INFINITY * (x * b.re + y * b.im);
234+
r.im = f64::INFINITY * (y * b.re - x * b.im);
235+
} else if (abs_breal.is_infinite() || abs_bimag.is_infinite())
236+
&& a.re.is_finite()
237+
&& a.im.is_finite()
238+
{
239+
let x = m::copysign(if b.re.is_infinite() { 1.0 } else { 0.0 }, b.re);
240+
let y = m::copysign(if b.im.is_infinite() { 1.0 } else { 0.0 }, b.im);
241+
r.re = 0.0 * (a.re * x + a.im * y);
242+
r.im = 0.0 * (a.im * x - a.re * y);
243+
}
244+
}
245+
246+
r
247+
}
248+
170249
/// Complex base-10 logarithm.
171250
#[inline]
172251
pub fn log10(z: Complex64) -> Result<Complex64> {
173-
let r = log(z)?;
252+
// Like log(z) without base, log10(0) raises EDOM
253+
if z.re == 0.0 && z.im == 0.0 {
254+
return Err(Error::EDOM);
255+
}
256+
let r = ln(z)?;
174257
Ok(Complex64::new(r.re / M_LN10, r.im / M_LN10))
175258
}
176259

@@ -191,13 +274,56 @@ mod tests {
191274
fn test_exp(re: f64, im: f64) {
192275
test_cmath_func("exp", exp, re, im);
193276
}
194-
fn test_log(re: f64, im: f64) {
195-
test_cmath_func("log", log, re, im);
277+
fn test_log_n(re: f64, im: f64) {
278+
test_cmath_func("log", |z| log(z, None), re, im);
196279
}
197280
fn test_log10(re: f64, im: f64) {
198281
test_cmath_func("log10", log10, re, im);
199282
}
200283

284+
/// Test log with base - compares with Python's cmath.log(z, base)
285+
fn test_log(z_re: f64, z_im: f64, base_re: f64, base_im: f64) {
286+
use pyo3::prelude::*;
287+
288+
let z = Complex64::new(z_re, z_im);
289+
let base = Complex64::new(base_re, base_im);
290+
let rs_result = log(z, Some(base));
291+
292+
pyo3::Python::attach(|py| {
293+
let cmath = pyo3::types::PyModule::import(py, "cmath").unwrap();
294+
let py_z = pyo3::types::PyComplex::from_doubles(py, z_re, z_im);
295+
let py_base = pyo3::types::PyComplex::from_doubles(py, base_re, base_im);
296+
let py_result = cmath.getattr("log").unwrap().call1((py_z, py_base));
297+
298+
match py_result {
299+
Ok(result) => {
300+
use pyo3::types::PyComplexMethods;
301+
let c = result.cast::<pyo3::types::PyComplex>().unwrap();
302+
let py_re = c.real();
303+
let py_im = c.imag();
304+
match rs_result {
305+
Ok(rs) => {
306+
crate::cmath::tests::assert_complex_eq(
307+
py_re, py_im, rs, "log", z_re, z_im,
308+
);
309+
}
310+
Err(e) => {
311+
panic!(
312+
"log({z_re}+{z_im}j, {base_re}+{base_im}j): py=({py_re}, {py_im}) but rs returned error {e:?}"
313+
);
314+
}
315+
}
316+
}
317+
Err(_) => {
318+
assert!(
319+
rs_result.is_err(),
320+
"log({z_re}+{z_im}j, {base_re}+{base_im}j): py raised error but rs={rs_result:?}"
321+
);
322+
}
323+
}
324+
});
325+
}
326+
201327
use crate::test::EDGE_VALUES;
202328

203329
#[test]
@@ -219,10 +345,10 @@ mod tests {
219345
}
220346

221347
#[test]
222-
fn edgetest_log() {
348+
fn edgetest_log_n() {
223349
for &re in &EDGE_VALUES {
224350
for &im in &EDGE_VALUES {
225-
test_log(re, im);
351+
test_log_n(re, im);
226352
}
227353
}
228354
}
@@ -236,6 +362,25 @@ mod tests {
236362
}
237363
}
238364

365+
#[test]
366+
fn edgetest_log() {
367+
// Test log with various bases - sign preservation edge cases
368+
let bases = [0.5, 2.0, 10.0];
369+
let values = [0.01, 0.1, 0.5, 1.0, 2.0, 10.0, 100.0];
370+
for &base in &bases {
371+
for &v in &values {
372+
test_log(v, 0.0, base, 0.0);
373+
}
374+
}
375+
// Additional edge cases with imaginary parts
376+
for &z_re in &EDGE_VALUES {
377+
for &z_im in &EDGE_VALUES {
378+
test_log(z_re, z_im, 2.0, 0.0);
379+
test_log(z_re, z_im, 0.5, 0.0);
380+
}
381+
}
382+
}
383+
239384
proptest::proptest! {
240385
#[test]
241386
fn proptest_sqrt(re: f64, im: f64) {
@@ -249,7 +394,7 @@ mod tests {
249394

250395
#[test]
251396
fn proptest_log(re: f64, im: f64) {
252-
test_log(re, im);
397+
test_log_n(re, im);
253398
}
254399

255400
#[test]

src/math.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ mod misc;
1010
mod trigonometric;
1111

1212
// Re-export from submodules
13-
pub use aggregate::{dist, fsum, prod, sumprod};
13+
pub use aggregate::{dist, fsum, prod, prod_int, sumprod, sumprod_int};
1414
pub use exponential::{cbrt, exp, exp2, expm1, log, log1p, log2, log10, pow, sqrt};
1515
pub use gamma::{erf, erfc, gamma, lgamma};
1616
pub use misc::{
17-
ceil, copysign, fabs, floor, fmod, frexp, isclose, isfinite, isinf, isnan, ldexp, modf,
17+
ceil, copysign, fabs, floor, fma, fmod, frexp, isclose, isfinite, isinf, isnan, ldexp, modf,
1818
nextafter, remainder, trunc, ulp,
1919
};
2020
pub use trigonometric::{

0 commit comments

Comments
 (0)