@@ -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]
172251pub 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]
0 commit comments