Skip to main content

ferray_ufunc/ops/
complex.rs

1// ferray-ufunc: Complex number functions
2//
3// real, imag, conj, conjugate, angle, abs (returns real magnitude)
4//
5// ## REQ status — REQ-11 (complex functions) + complex transcendentals
6//
7// SHIPPED:
8//   - REQ-11 (`real`/`imag`/`conj`/`conjugate`/`angle`/`abs`): the NumPy
9//     complex-attribute ufunc family over `Array<Complex<T>, D>` (REQ-1
10//     dimensionality preserved). Anchors: `pub fn real`/`pub fn imag` (return
11//     `Array<T, D>`), `pub fn conj`/`pub fn conjugate`, `pub fn angle`
12//     (phase, returns real), `pub fn abs` (real magnitude via `hypot` —
13//     the `hypot`-based magnitude that avoids spurious overflow was fixed in
14//     #1084, matching `np.abs(complex)`). Predicates `pub fn isreal`/
15//     `pub fn iscomplex`/`pub fn isrealobj`/`pub fn iscomplexobj`/
16//     `pub fn isscalar` round out the numpy complex-introspection surface.
17//   - Complex transcendentals: `pub fn sqrt_complex`/`pub fn ln_complex`/
18//     `pub fn log2_complex`/`pub fn log10_complex`/`pub fn log1p_complex`/
19//     `pub fn exp_complex`/`pub fn expm1_complex`/`pub fn power_complex` and
20//     the inverse/hyperbolic family `pub fn acos_complex`/`pub fn asin_complex`/
21//     `pub fn atan_complex`/`pub fn asinh_complex`/`pub fn acosh_complex`/
22//     `pub fn atanh_complex` (+ `sin_complex`/`cos_complex`/`tan_complex`/
23//     `sinh_complex`/`cosh_complex`/`tanh_complex`) provide `np.sin(complex)`
24//     parity. Branch cuts audited against numpy 2.4.x and green; these are the
25//     production callees for the ferray-python `numpy.emath` submodule
26//     (REQ-27, `ferray-python/src/emath.rs`).
27//   - Non-test production consumer: re-exported verbatim from the crate root
28//     (`lib.rs` `pub use ops::complex::{abs, angle, conj, conjugate, imag,
29//     iscomplex, …, real}` plus the `*_complex` transcendental block), the
30//     public complex surface, the ferray-python complex binding, and the
31//     `numpy.emath` domain-promotion routing target.
32//
33// NOT-STARTED: none — REQ-11 is fully shipped for this module.
34
35use ferray_core::Array;
36use ferray_core::dimension::Dimension;
37use ferray_core::dtype::Element;
38use ferray_core::error::FerrayResult;
39use num_complex::Complex;
40use num_traits::Float;
41
42/// Extract the real part of a complex array.
43///
44/// Works with `Complex<f32>` and `Complex<f64>` arrays.
45pub fn real<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
46where
47    T: Element + Float,
48    Complex<T>: Element,
49    D: Dimension,
50{
51    let data: Vec<T> = input.iter().map(|c| c.re).collect();
52    Array::from_vec(input.dim().clone(), data)
53}
54
55/// Extract the imaginary part of a complex array.
56pub fn imag<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
57where
58    T: Element + Float,
59    Complex<T>: Element,
60    D: Dimension,
61{
62    let data: Vec<T> = input.iter().map(|c| c.im).collect();
63    Array::from_vec(input.dim().clone(), data)
64}
65
66/// Compute the complex conjugate.
67pub fn conj<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
68where
69    T: Element + Float,
70    Complex<T>: Element,
71    D: Dimension,
72{
73    let data: Vec<Complex<T>> = input.iter().map(num_complex::Complex::conj).collect();
74    Array::from_vec(input.dim().clone(), data)
75}
76
77/// Alias for [`conj`].
78pub fn conjugate<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
79where
80    T: Element + Float,
81    Complex<T>: Element,
82    D: Dimension,
83{
84    conj(input)
85}
86
87/// Compute the angle (argument/phase) of complex numbers.
88///
89/// Returns values in radians, in the range [-pi, pi].
90pub fn angle<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
91where
92    T: Element + Float,
93    Complex<T>: Element,
94    D: Dimension,
95{
96    let data: Vec<T> = input.iter().map(|c| c.im.atan2(c.re)).collect();
97    Array::from_vec(input.dim().clone(), data)
98}
99
100/// Compute the absolute value (magnitude) of complex numbers.
101///
102/// Returns a real array.
103pub fn abs<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
104where
105    T: Element + Float,
106    Complex<T>: Element,
107    D: Dimension,
108{
109    // Use C99 overflow-safe hypot (== numpy's npy_cabs/npy_hypot) instead of
110    // the naive `(re*re + im*im).sqrt()`, which overflows for large magnitudes,
111    // underflows for tiny ones, and yields NaN where C99 hypot yields +inf
112    // (hypot(inf, x) == inf for any x, including NaN). `Float::hypot` delegates
113    // to the std/libm hypot, matching numpy bit-for-bit on these edge cases.
114    let data: Vec<T> = input.iter().map(|c| c.re.hypot(c.im)).collect();
115    Array::from_vec(input.dim().clone(), data)
116}
117
118// ---------------------------------------------------------------------------
119// Complex predicates: iscomplex / isreal / iscomplexobj / isrealobj / isscalar
120// ---------------------------------------------------------------------------
121
122/// Element-wise: true where the imaginary part is non-zero.
123///
124/// Analogous to `numpy.iscomplex(x)`. NumPy returns `False` everywhere for
125/// real-dtype inputs; in Rust, real-dtype arrays are statically distinct
126/// types, so call [`iscomplex_real`] instead for that case.
127pub fn iscomplex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<bool, D>>
128where
129    T: Element + Float,
130    Complex<T>: Element,
131    D: Dimension,
132{
133    let zero = <T as num_traits::Zero>::zero();
134    let data: Vec<bool> = input.iter().map(|c| c.im != zero).collect();
135    Array::from_vec(input.dim().clone(), data)
136}
137
138/// Real-input variant of [`iscomplex`]: always returns `false` everywhere.
139///
140/// Provided so generic code can ask "is this element complex-valued" against
141/// a real-dtype array without a separate type branch. Matches NumPy's
142/// `iscomplex` behavior on real inputs.
143pub fn iscomplex_real<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
144where
145    T: Element,
146    D: Dimension,
147{
148    let data = vec![false; input.size()];
149    Array::from_vec(input.dim().clone(), data)
150}
151
152/// Element-wise: true where the imaginary part is zero.
153///
154/// Analogous to `numpy.isreal(x)` for complex inputs. For real-dtype arrays
155/// see [`isreal_real`] (always-true).
156pub fn isreal<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<bool, D>>
157where
158    T: Element + Float,
159    Complex<T>: Element,
160    D: Dimension,
161{
162    let zero = <T as num_traits::Zero>::zero();
163    let data: Vec<bool> = input.iter().map(|c| c.im == zero).collect();
164    Array::from_vec(input.dim().clone(), data)
165}
166
167/// Real-input variant of [`isreal`]: always returns `true` everywhere.
168pub fn isreal_real<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
169where
170    T: Element,
171    D: Dimension,
172{
173    let data = vec![true; input.size()];
174    Array::from_vec(input.dim().clone(), data)
175}
176
177/// Whether the array's element dtype is a complex type.
178///
179/// Analogous to `numpy.iscomplexobj(x)` — returns a single `bool` based on
180/// the dtype, not a per-element check.
181pub fn iscomplexobj<T, D>(_: &Array<T, D>) -> bool
182where
183    T: Element,
184    D: Dimension,
185{
186    T::dtype().is_complex()
187}
188
189/// Whether the array's element dtype is a real (non-complex) type.
190///
191/// Analogous to `numpy.isrealobj(x)`. Returns `!iscomplexobj(x)`.
192pub fn isrealobj<T, D>(a: &Array<T, D>) -> bool
193where
194    T: Element,
195    D: Dimension,
196{
197    !iscomplexobj(a)
198}
199
200/// Whether the array represents a scalar (0-D).
201///
202/// Analogous to `numpy.isscalar(x)`. In Rust, dimensionality is statically
203/// typed, so this returns `true` only for 0-D arrays (`Ix0` or `IxDyn` with
204/// empty shape).
205pub fn isscalar<T, D>(a: &Array<T, D>) -> bool
206where
207    T: Element,
208    D: Dimension,
209{
210    a.shape().is_empty()
211}
212
213// ---------------------------------------------------------------------------
214// Complex transcendentals — sin/cos/tan, sinh/cosh/tanh, exp/log/sqrt,
215// arc* / arch* and friends. NumPy provides these for complex dtypes via
216// loops_unary_complex.dispatch.c.src; ferray's existing real-only ufuncs
217// (T: Float bound) exclude Complex<f32>/<f64>, so we ship them in this
218// module and re-export at the crate root.
219//
220// All implementations delegate to the corresponding `Complex::*` method
221// from num_complex, which uses well-known identities (e.g. exp(z) =
222// exp(z.re) * (cos(z.im) + i*sin(z.im))).
223// ---------------------------------------------------------------------------
224
225macro_rules! complex_unary {
226    ($fn_name:ident, $method:ident, $doc:expr) => {
227        #[doc = $doc]
228        pub fn $fn_name<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
229        where
230            T: Element + Float,
231            Complex<T>: Element,
232            D: Dimension,
233        {
234            let data: Vec<Complex<T>> = input.iter().map(|z| z.$method()).collect();
235            Array::from_vec(input.dim().clone(), data)
236        }
237    };
238}
239
240// ---------------------------------------------------------------------------
241// C99 signed-zero branch-cut correction for the inverse trig / hyperbolic
242// functions (arcsin/arccos/arctanh and arcsinh/arctan/arccosh).
243//
244// num_complex's `asin`/`acos`/`atanh`/`asinh`/`atan`/`acosh` choose the
245// branch-cut side from the sign of a *non-zero* component (e.g. `sign(re)`)
246// and IGNORE the sign of a signed ZERO on the cut. numpy follows C99: on the
247// real-axis cut (`im == ±0`, for asin/acos/atanh/acosh) the result's imaginary
248// sign is fixed by the sign of the input's imaginary signed-zero; on the
249// imaginary-axis cut (`re == ±0`, for asinh/atan) the result's real sign is
250// fixed by the input's real signed-zero. Upstream defines casin/catan via
251// casinh/catanh with conjugation (npy_math_complex.c.src:1352/1362) and selects
252// the cut side with `npy_signbit` (`:1453`) — exactly the identity used here:
253// `f(conj(z)) == conj(f(z))` yields the two branches, and we pick the one whose
254// relevant component sign matches the input's signed-zero (with `follow=false`
255// for arccos, whose imaginary sign is the negation — `arccos = π/2 − arcsin`).
256//
257// Interior points (`im != 0` / `re != 0`) bypass these helpers entirely and
258// stay byte-for-byte what num_complex returns (already matching numpy).
259
260/// On the real-axis cut (`z.im == 0`) pick the C99 branch whose imaginary sign
261/// matches the input's imaginary signed-zero (`follow`), else its negation.
262/// Off the cut, return `f(z)` unchanged.
263fn fix_real_axis_cut<T>(
264    z: Complex<T>,
265    follow: bool,
266    f: impl Fn(Complex<T>) -> Complex<T>,
267) -> Complex<T>
268where
269    T: Element + Float,
270{
271    let zero = <T as num_traits::Zero>::zero();
272    if z.im == zero {
273        let base = f(z);
274        let want_neg = z.im.is_sign_negative() == follow;
275        if base.im.is_sign_negative() == want_neg {
276            base
277        } else {
278            f(z.conj()).conj()
279        }
280    } else {
281        f(z)
282    }
283}
284
285/// On the imaginary-axis cut (`z.re == 0`) pick the C99 branch whose real sign
286/// matches the input's real signed-zero. Off the cut, return `f(z)` unchanged.
287fn fix_imag_axis_cut<T>(z: Complex<T>, f: impl Fn(Complex<T>) -> Complex<T>) -> Complex<T>
288where
289    T: Element + Float,
290{
291    let zero = <T as num_traits::Zero>::zero();
292    if z.re == zero {
293        let base = f(z);
294        let want_neg = z.re.is_sign_negative();
295        if base.re.is_sign_negative() == want_neg {
296            base
297        } else {
298            f(z.conj()).conj()
299        }
300    } else {
301        f(z)
302    }
303}
304
305complex_unary!(
306    sin_complex,
307    sin,
308    "Element-wise complex sin: sin(z) = sin(z.re)cosh(z.im) + i cos(z.re)sinh(z.im)."
309);
310complex_unary!(cos_complex, cos, "Element-wise complex cos.");
311complex_unary!(tan_complex, tan, "Element-wise complex tan.");
312complex_unary!(sinh_complex, sinh, "Element-wise complex sinh.");
313complex_unary!(cosh_complex, cosh, "Element-wise complex cosh.");
314complex_unary!(tanh_complex, tanh, "Element-wise complex tanh.");
315
316/// Element-wise complex arcsin. On the real-axis branch cut (`im == ±0`,
317/// `|re| > 1`) the imaginary sign follows the input's signed-zero per C99.
318pub fn asin_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
319where
320    T: Element + Float,
321    Complex<T>: Element,
322    D: Dimension,
323{
324    let data: Vec<Complex<T>> = input
325        .iter()
326        .map(|z| fix_real_axis_cut(*z, true, |w| w.asin()))
327        .collect();
328    Array::from_vec(input.dim().clone(), data)
329}
330
331/// Element-wise complex arccos. On the real-axis branch cut (`im == ±0`,
332/// `|re| > 1`) the imaginary sign is the negation of the input's signed-zero
333/// per C99 (`arccos = π/2 − arcsin`).
334pub fn acos_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
335where
336    T: Element + Float,
337    Complex<T>: Element,
338    D: Dimension,
339{
340    let data: Vec<Complex<T>> = input
341        .iter()
342        .map(|z| fix_real_axis_cut(*z, false, |w| w.acos()))
343        .collect();
344    Array::from_vec(input.dim().clone(), data)
345}
346
347/// Element-wise complex arctan. On the imaginary-axis branch cut (`re == ±0`,
348/// `|im| > 1`) the real sign follows the input's real signed-zero per C99.
349pub fn atan_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
350where
351    T: Element + Float,
352    Complex<T>: Element,
353    D: Dimension,
354{
355    let data: Vec<Complex<T>> = input
356        .iter()
357        .map(|z| fix_imag_axis_cut(*z, |w| w.atan()))
358        .collect();
359    Array::from_vec(input.dim().clone(), data)
360}
361
362/// Element-wise complex arcsinh. On the imaginary-axis branch cut (`re == ±0`,
363/// `|im| > 1`) the real sign follows the input's real signed-zero per C99.
364pub fn asinh_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
365where
366    T: Element + Float,
367    Complex<T>: Element,
368    D: Dimension,
369{
370    let data: Vec<Complex<T>> = input
371        .iter()
372        .map(|z| fix_imag_axis_cut(*z, |w| w.asinh()))
373        .collect();
374    Array::from_vec(input.dim().clone(), data)
375}
376
377/// Element-wise complex arccosh. On the real-axis branch cut (`im == ±0`,
378/// `re < 1`) the imaginary sign follows the input's signed-zero per C99.
379pub fn acosh_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
380where
381    T: Element + Float,
382    Complex<T>: Element,
383    D: Dimension,
384{
385    let data: Vec<Complex<T>> = input
386        .iter()
387        .map(|z| fix_real_axis_cut(*z, true, |w| w.acosh()))
388        .collect();
389    Array::from_vec(input.dim().clone(), data)
390}
391
392/// Element-wise complex arctanh. On the real-axis branch cut (`im == ±0`,
393/// `|re| > 1`) the imaginary sign follows the input's signed-zero per C99.
394pub fn atanh_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
395where
396    T: Element + Float,
397    Complex<T>: Element,
398    D: Dimension,
399{
400    let data: Vec<Complex<T>> = input
401        .iter()
402        .map(|z| fix_real_axis_cut(*z, true, |w| w.atanh()))
403        .collect();
404    Array::from_vec(input.dim().clone(), data)
405}
406complex_unary!(
407    exp_complex,
408    exp,
409    "Element-wise complex exp: exp(z) = exp(z.re) (cos(z.im) + i sin(z.im))."
410);
411complex_unary!(
412    ln_complex,
413    ln,
414    "Element-wise complex natural log: log(z) = log|z| + i arg(z), branch cut along negative real axis."
415);
416complex_unary!(
417    sqrt_complex,
418    sqrt,
419    "Element-wise complex sqrt; principal branch (Re(sqrt(z)) >= 0)."
420);
421
422/// Element-wise complex log2: ln(z) / ln(2).
423pub fn log2_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
424where
425    T: Element + Float,
426    Complex<T>: Element,
427    D: Dimension,
428{
429    let one = <T as num_traits::One>::one();
430    let ln2: T = <T as num_traits::NumCast>::from(core::f64::consts::LN_2).unwrap();
431    let inv_ln2 = one / ln2;
432    let data: Vec<Complex<T>> = input
433        .iter()
434        .map(|z| {
435            let l = z.ln();
436            Complex::new(l.re * inv_ln2, l.im * inv_ln2)
437        })
438        .collect();
439    Array::from_vec(input.dim().clone(), data)
440}
441
442/// Element-wise complex log10: ln(z) / ln(10).
443pub fn log10_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
444where
445    T: Element + Float,
446    Complex<T>: Element,
447    D: Dimension,
448{
449    let one = <T as num_traits::One>::one();
450    let ln10: T = <T as num_traits::NumCast>::from(core::f64::consts::LN_10).unwrap();
451    let inv_ln10 = one / ln10;
452    let data: Vec<Complex<T>> = input
453        .iter()
454        .map(|z| {
455            let l = z.ln();
456            Complex::new(l.re * inv_ln10, l.im * inv_ln10)
457        })
458        .collect();
459    Array::from_vec(input.dim().clone(), data)
460}
461
462/// Element-wise complex `exp(z) - 1`. For small |z|, naive `exp(z)-1`
463/// suffers catastrophic cancellation; this routine uses the identity
464/// `expm1(z) = expm1(z.re) cos(z.im) + (1+expm1(z.re)) (cos(z.im)-1) + i exp(z.re) sin(z.im)`
465/// rearranged to preserve precision near 0.
466pub fn expm1_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
467where
468    T: Element + Float,
469    Complex<T>: Element,
470    D: Dimension,
471{
472    let one = <T as num_traits::One>::one();
473    let two = one + one;
474    let half: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
475    let data: Vec<Complex<T>> = input
476        .iter()
477        .map(|z| {
478            // exp(z) - 1 = (exp(re) cos(im) - 1) + i exp(re) sin(im)
479            //            = (exp(re)-1) cos(im) + (cos(im)-1) + i exp(re) sin(im)
480            let er_m1 = z.re.exp_m1();
481            let er = er_m1 + one;
482            let cos_im = z.im.cos();
483            let sin_im = z.im.sin();
484            // Use cos(im) - 1 = -2 sin²(im/2) for precision near 0.
485            let s = (z.im * half).sin();
486            let cos_im_m1 = -two * s * s;
487            Complex::new(er_m1 * cos_im + cos_im_m1, er * sin_im)
488        })
489        .collect();
490    Array::from_vec(input.dim().clone(), data)
491}
492
493/// Element-wise complex `log(1 + z)`. Reduces to the equivalent of
494/// `log1p` on the real axis but uses `Complex::ln(1 + z)` directly for
495/// off-axis values; precision near z=0 is preserved by routing through
496/// the identity `log1p(z) = log1p(re) + i atan2(im, 1+re)` when |z| is
497/// small, otherwise falls back to ln(1+z).
498pub fn log1p_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
499where
500    T: Element + Float,
501    Complex<T>: Element,
502    D: Dimension,
503{
504    let one = <T as num_traits::One>::one();
505    let zero = <T as num_traits::Zero>::zero();
506    let two = one + one;
507    let half: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
508    let small: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
509    let data: Vec<Complex<T>> = input
510        .iter()
511        .map(|z| {
512            if z.re.abs() < small && z.im.abs() < small {
513                // log(1+z) where |z| small: Re = 0.5*ln((1+re)^2 + im^2);
514                // expand (1+re)^2 + im^2 - 1 = 2 re + re^2 + im^2 = re*(2+re) + im^2.
515                let u = z.re * (two + z.re) + z.im * z.im;
516                let half_log = half * u.ln_1p();
517                let arg = z.im.atan2(one + z.re);
518                Complex::new(half_log, arg)
519            } else {
520                (Complex::new(one, zero) + *z).ln()
521            }
522        })
523        .collect();
524    Array::from_vec(input.dim().clone(), data)
525}
526
527/// Element-wise complex power `z^w` with broadcasting against a scalar
528/// exponent `w`. For complex z and complex w: `z^w = exp(w * log(z))`.
529pub fn power_complex<T, D>(
530    base: &Array<Complex<T>, D>,
531    exponent: Complex<T>,
532) -> FerrayResult<Array<Complex<T>, D>>
533where
534    T: Element + Float,
535    Complex<T>: Element,
536    D: Dimension,
537{
538    let data: Vec<Complex<T>> = base.iter().map(|z| z.powc(exponent)).collect();
539    Array::from_vec(base.dim().clone(), data)
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545    use ferray_core::dimension::Ix1;
546    use num_complex::Complex64;
547
548    fn arr1_c64(data: Vec<Complex64>) -> Array<Complex64, Ix1> {
549        let n = data.len();
550        Array::from_vec(Ix1::new([n]), data).unwrap()
551    }
552
553    #[test]
554    fn test_real() {
555        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)]);
556        let r = real(&a).unwrap();
557        assert_eq!(r.as_slice().unwrap(), &[1.0, 3.0]);
558    }
559
560    #[test]
561    fn test_imag() {
562        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)]);
563        let r = imag(&a).unwrap();
564        assert_eq!(r.as_slice().unwrap(), &[2.0, 4.0]);
565    }
566
567    #[test]
568    fn test_conj() {
569        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)]);
570        let r = conj(&a).unwrap();
571        let s = r.as_slice().unwrap();
572        assert_eq!(s[0], Complex64::new(1.0, -2.0));
573        assert_eq!(s[1], Complex64::new(3.0, 4.0));
574    }
575
576    #[test]
577    fn test_conjugate_alias() {
578        let a = arr1_c64(vec![Complex64::new(1.0, 2.0)]);
579        let r = conjugate(&a).unwrap();
580        assert_eq!(r.as_slice().unwrap()[0], Complex64::new(1.0, -2.0));
581    }
582
583    #[test]
584    fn test_angle() {
585        let a = arr1_c64(vec![
586            Complex64::new(1.0, 0.0),
587            Complex64::new(0.0, 1.0),
588            Complex64::new(-1.0, 0.0),
589        ]);
590        let r = angle(&a).unwrap();
591        let s = r.as_slice().unwrap();
592        assert!((s[0] - 0.0).abs() < 1e-12);
593        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
594        assert!((s[2] - std::f64::consts::PI).abs() < 1e-12);
595    }
596
597    #[test]
598    fn test_abs() {
599        let a = arr1_c64(vec![Complex64::new(3.0, 4.0), Complex64::new(0.0, 1.0)]);
600        let r = abs(&a).unwrap();
601        let s = r.as_slice().unwrap();
602        assert!((s[0] - 5.0).abs() < 1e-12);
603        assert!((s[1] - 1.0).abs() < 1e-12);
604    }
605
606    #[test]
607    fn test_iscomplex_complex_input() {
608        let a = arr1_c64(vec![
609            Complex64::new(1.0, 0.0),
610            Complex64::new(2.0, 1.5),
611            Complex64::new(0.0, 0.0),
612        ]);
613        let r = iscomplex(&a).unwrap();
614        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
615    }
616
617    #[test]
618    fn test_isreal_complex_input() {
619        let a = arr1_c64(vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 1.5)]);
620        let r = isreal(&a).unwrap();
621        assert_eq!(r.as_slice().unwrap(), &[true, false]);
622    }
623
624    #[test]
625    fn test_iscomplex_real_input() {
626        let a: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
627        let r = iscomplex_real(&a).unwrap();
628        assert_eq!(r.as_slice().unwrap(), &[false, false, false]);
629    }
630
631    #[test]
632    fn test_isreal_real_input() {
633        let a: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
634        let r = isreal_real(&a).unwrap();
635        assert_eq!(r.as_slice().unwrap(), &[true, true, true]);
636    }
637
638    #[test]
639    fn test_iscomplexobj_isrealobj() {
640        let c = arr1_c64(vec![Complex64::new(1.0, 2.0)]);
641        assert!(iscomplexobj(&c));
642        assert!(!isrealobj(&c));
643
644        let r: Array<f64, Ix1> = Array::from_vec(Ix1::new([2]), vec![1.0, 2.0]).unwrap();
645        assert!(!iscomplexobj(&r));
646        assert!(isrealobj(&r));
647    }
648
649    fn approx_eq_c(a: Complex64, b: Complex64, eps: f64) -> bool {
650        (a.re - b.re).abs() <= eps && (a.im - b.im).abs() <= eps
651    }
652
653    #[test]
654    fn test_sin_complex_matches_identity() {
655        // sin(i) = i sinh(1) ≈ i*1.1752011936438014
656        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0), Complex64::new(1.0, 1.0)]);
657        let out = sin_complex(&arr).unwrap();
658        let v0 = out.iter().next().copied().unwrap();
659        assert!(approx_eq_c(
660            v0,
661            Complex64::new(0.0, 1.1752011936438014),
662            1e-10
663        ));
664    }
665
666    #[test]
667    fn test_cos_complex_matches_identity() {
668        // cos(i) = cosh(1) ≈ 1.5430806348152437
669        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0)]);
670        let out = cos_complex(&arr).unwrap();
671        let v0 = out.iter().next().copied().unwrap();
672        assert!(approx_eq_c(
673            v0,
674            Complex64::new(1.5430806348152437, 0.0),
675            1e-10
676        ));
677    }
678
679    #[test]
680    fn test_exp_complex_eulers_identity() {
681        // e^{i*pi} = -1 (Euler's identity).
682        let arr = arr1_c64(vec![Complex64::new(0.0, std::f64::consts::PI)]);
683        let out = exp_complex(&arr).unwrap();
684        let v0 = out.iter().next().copied().unwrap();
685        assert!(approx_eq_c(v0, Complex64::new(-1.0, 0.0), 1e-12));
686    }
687
688    #[test]
689    fn test_ln_complex_inverse_of_exp() {
690        let z = Complex64::new(0.7, 1.3);
691        let arr = arr1_c64(vec![z.exp()]);
692        let out = ln_complex(&arr).unwrap();
693        let v = out.iter().next().copied().unwrap();
694        assert!(approx_eq_c(v, z, 1e-12));
695    }
696
697    #[test]
698    fn test_sqrt_complex_principal_branch() {
699        // sqrt(i) = (1+i)/sqrt(2)
700        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0)]);
701        let out = sqrt_complex(&arr).unwrap();
702        let v = out.iter().next().copied().unwrap();
703        let inv_sqrt2 = 1.0_f64 / 2.0_f64.sqrt();
704        assert!(approx_eq_c(v, Complex64::new(inv_sqrt2, inv_sqrt2), 1e-12));
705    }
706
707    #[test]
708    fn test_log2_complex() {
709        // log2(2 + 0i) = 1
710        let arr = arr1_c64(vec![Complex64::new(2.0, 0.0)]);
711        let out = log2_complex(&arr).unwrap();
712        let v = out.iter().next().copied().unwrap();
713        assert!(approx_eq_c(v, Complex64::new(1.0, 0.0), 1e-12));
714    }
715
716    #[test]
717    fn test_log10_complex() {
718        // log10(100 + 0i) = 2
719        let arr = arr1_c64(vec![Complex64::new(100.0, 0.0)]);
720        let out = log10_complex(&arr).unwrap();
721        let v = out.iter().next().copied().unwrap();
722        assert!(approx_eq_c(v, Complex64::new(2.0, 0.0), 1e-12));
723    }
724
725    #[test]
726    fn test_expm1_complex_small_arg_precision() {
727        // For very tiny z, expm1(z) ≈ z + z²/2 + ..., dominated by z.
728        // The naive `z.exp() - 1` suffers catastrophic cancellation here.
729        // Our implementation routes through exp_m1 + sin² identity to
730        // preserve the leading Taylor term. Tolerance is loose because
731        // the 2nd-order term and rounding both contribute at the
732        // ~1e-12 * f64-eps ≈ 1e-28 scale, which dominates the cos·im_m1
733        // fixup arithmetic.
734        let z = Complex64::new(1e-12, 1e-12);
735        let arr = arr1_c64(vec![z]);
736        let out = expm1_complex(&arr).unwrap();
737        let v = out.iter().next().copied().unwrap();
738        // Result must be close to z (Taylor leading term) within a few
739        // ULPs of the magnitude. f64 ULP for 1e-12 ≈ 2.22e-28.
740        let rel_err = (v - z).norm() / z.norm();
741        assert!(
742            rel_err < 1e-4,
743            "expm1 small-arg rel_err = {rel_err:e}, v = {v:?}"
744        );
745    }
746
747    #[test]
748    fn test_log1p_complex_small_arg_precision() {
749        // log1p(z) for tiny z ≈ z - z²/2 + ..., dominated by z.
750        let z = Complex64::new(1e-10, 1e-10);
751        let arr = arr1_c64(vec![z]);
752        let out = log1p_complex(&arr).unwrap();
753        let v = out.iter().next().copied().unwrap();
754        let rel_err = (v - z).norm() / z.norm();
755        assert!(
756            rel_err < 1e-4,
757            "log1p small-arg rel_err = {rel_err:e}, v = {v:?}"
758        );
759    }
760
761    #[test]
762    fn test_expm1_complex_moderate_arg_matches_naive() {
763        // For moderate-magnitude z, expm1 should agree with z.exp() - 1.
764        let z = Complex64::new(0.5, 0.3);
765        let arr = arr1_c64(vec![z]);
766        let out = expm1_complex(&arr).unwrap();
767        let v = out.iter().next().copied().unwrap();
768        let expected = z.exp() - Complex64::new(1.0, 0.0);
769        assert!(approx_eq_c(v, expected, 1e-12));
770    }
771
772    #[test]
773    fn test_log1p_complex_moderate_arg_matches_naive() {
774        let z = Complex64::new(0.4, 0.2);
775        let arr = arr1_c64(vec![z]);
776        let out = log1p_complex(&arr).unwrap();
777        let v = out.iter().next().copied().unwrap();
778        let expected = (Complex64::new(1.0, 0.0) + z).ln();
779        assert!(approx_eq_c(v, expected, 1e-12));
780    }
781
782    #[test]
783    fn test_power_complex_integer_exponent() {
784        // (1+i)^2 = 2i
785        let arr = arr1_c64(vec![Complex64::new(1.0, 1.0)]);
786        let out = power_complex(&arr, Complex64::new(2.0, 0.0)).unwrap();
787        let v = out.iter().next().copied().unwrap();
788        assert!(approx_eq_c(v, Complex64::new(0.0, 2.0), 1e-12));
789    }
790
791    #[test]
792    fn test_tanh_complex_round_trip() {
793        // atanh(tanh(z)) ≈ z (within principal branch).
794        let z = Complex64::new(0.5, 0.3);
795        let arr = arr1_c64(vec![z]);
796        let t = tanh_complex(&arr).unwrap();
797        let r = atanh_complex(&t).unwrap();
798        let v = r.iter().next().copied().unwrap();
799        assert!(approx_eq_c(v, z, 1e-12));
800    }
801
802    // C99 signed-zero branch-cut sign tests. Expected values derived LIVE from
803    // numpy 2.4.5 (R-CHAR-3), e.g.:
804    //   np.arcsin(2+0j)  = (1.5707963267948966 + 1.3169578969248166j)
805    //   np.arcsin(2-0j)  = (1.5707963267948966 - 1.3169578969248166j)
806    //   np.arccos(2+0j)  = (0 - 1.3169578969248166j)
807    //   np.arctanh(2+0j) = (0.5493061443340549 + 1.5707963267948966j)
808    //   np.arcsinh(-0+2j)= (-1.3169578969248166 + 1.5707963267948966j)
809    //   np.arctan(-0+2j) = (-1.5707963267948966 + 0.5493061443340549j)
810    //   np.arccosh(-2-0j)= (1.3169578969248166 - 3.141592653589793j)
811    const MAG_S: f64 = 1.3169578969248166; // arcsin/arccos/arctanh magnitude at |2|
812    const HALF_PI: f64 = std::f64::consts::FRAC_PI_2;
813    const PI: f64 = std::f64::consts::PI;
814
815    type ComplexUnaryFn = fn(&Array<Complex64, Ix1>) -> FerrayResult<Array<Complex64, Ix1>>;
816
817    fn call1(z: Complex64, f: ComplexUnaryFn) -> Complex64 {
818        match f(&arr1_c64(vec![z]))
819            .ok()
820            .and_then(|a| a.iter().next().copied())
821        {
822            Some(v) => v,
823            None => Complex64::new(f64::NAN, f64::NAN),
824        }
825    }
826
827    #[test]
828    fn test_arcsin_branch_cut_signed_zero() {
829        // im follows input zero sign; re follows sign(re).
830        assert!(approx_eq_c(
831            call1(Complex64::new(2.0, 0.0), asin_complex),
832            Complex64::new(HALF_PI, MAG_S),
833            1e-12
834        ));
835        assert!(approx_eq_c(
836            call1(Complex64::new(2.0, -0.0), asin_complex),
837            Complex64::new(HALF_PI, -MAG_S),
838            1e-12
839        ));
840        assert!(approx_eq_c(
841            call1(Complex64::new(-2.0, 0.0), asin_complex),
842            Complex64::new(-HALF_PI, MAG_S),
843            1e-12
844        ));
845        assert!(approx_eq_c(
846            call1(Complex64::new(-2.0, -0.0), asin_complex),
847            Complex64::new(-HALF_PI, -MAG_S),
848            1e-12
849        ));
850    }
851
852    #[test]
853    fn test_arccos_branch_cut_signed_zero() {
854        // im is the NEGATION of input zero sign.
855        assert!(approx_eq_c(
856            call1(Complex64::new(2.0, 0.0), acos_complex),
857            Complex64::new(0.0, -MAG_S),
858            1e-12
859        ));
860        assert!(approx_eq_c(
861            call1(Complex64::new(2.0, -0.0), acos_complex),
862            Complex64::new(0.0, MAG_S),
863            1e-12
864        ));
865        assert!(approx_eq_c(
866            call1(Complex64::new(-2.0, 0.0), acos_complex),
867            Complex64::new(PI, -MAG_S),
868            1e-12
869        ));
870        assert!(approx_eq_c(
871            call1(Complex64::new(-2.0, -0.0), acos_complex),
872            Complex64::new(PI, MAG_S),
873            1e-12
874        ));
875    }
876
877    #[test]
878    fn test_arctanh_branch_cut_signed_zero() {
879        let re_m = 0.5493061443340549;
880        assert!(approx_eq_c(
881            call1(Complex64::new(2.0, 0.0), atanh_complex),
882            Complex64::new(re_m, HALF_PI),
883            1e-12
884        ));
885        assert!(approx_eq_c(
886            call1(Complex64::new(2.0, -0.0), atanh_complex),
887            Complex64::new(re_m, -HALF_PI),
888            1e-12
889        ));
890        assert!(approx_eq_c(
891            call1(Complex64::new(-2.0, 0.0), atanh_complex),
892            Complex64::new(-re_m, HALF_PI),
893            1e-12
894        ));
895        assert!(approx_eq_c(
896            call1(Complex64::new(-2.0, -0.0), atanh_complex),
897            Complex64::new(-re_m, -HALF_PI),
898            1e-12
899        ));
900    }
901
902    #[test]
903    fn test_arcsinh_branch_cut_signed_zero() {
904        // imaginary-axis cut: re follows input REAL zero sign.
905        assert!(approx_eq_c(
906            call1(Complex64::new(0.0, 2.0), asinh_complex),
907            Complex64::new(MAG_S, HALF_PI),
908            1e-12
909        ));
910        assert!(approx_eq_c(
911            call1(Complex64::new(-0.0, 2.0), asinh_complex),
912            Complex64::new(-MAG_S, HALF_PI),
913            1e-12
914        ));
915        assert!(approx_eq_c(
916            call1(Complex64::new(0.0, -2.0), asinh_complex),
917            Complex64::new(MAG_S, -HALF_PI),
918            1e-12
919        ));
920        assert!(approx_eq_c(
921            call1(Complex64::new(-0.0, -2.0), asinh_complex),
922            Complex64::new(-MAG_S, -HALF_PI),
923            1e-12
924        ));
925    }
926
927    #[test]
928    fn test_arctan_branch_cut_signed_zero() {
929        let im_m = 0.5493061443340549;
930        assert!(approx_eq_c(
931            call1(Complex64::new(0.0, 2.0), atan_complex),
932            Complex64::new(HALF_PI, im_m),
933            1e-12
934        ));
935        assert!(approx_eq_c(
936            call1(Complex64::new(-0.0, 2.0), atan_complex),
937            Complex64::new(-HALF_PI, im_m),
938            1e-12
939        ));
940    }
941
942    #[test]
943    fn test_arccosh_branch_cut_signed_zero() {
944        // re<1 real-axis cut: im follows input imaginary zero sign.
945        assert!(approx_eq_c(
946            call1(Complex64::new(-2.0, 0.0), acosh_complex),
947            Complex64::new(MAG_S, PI),
948            1e-12
949        ));
950        assert!(approx_eq_c(
951            call1(Complex64::new(-2.0, -0.0), acosh_complex),
952            Complex64::new(MAG_S, -PI),
953            1e-12
954        ));
955        // arccosh(0.5+0j) = (0 + 1.0471975511965976j) ; (0.5-0j) -> conjugate.
956        let m = 1.0471975511965976;
957        assert!(approx_eq_c(
958            call1(Complex64::new(0.5, 0.0), acosh_complex),
959            Complex64::new(0.0, m),
960            1e-12
961        ));
962        assert!(approx_eq_c(
963            call1(Complex64::new(0.5, -0.0), acosh_complex),
964            Complex64::new(0.0, -m),
965            1e-12
966        ));
967    }
968
969    #[test]
970    fn test_arc_interior_points_unchanged() {
971        // Interior points (im != 0 / re != 0) must equal bare num_complex.
972        let z = Complex64::new(1.0, 1.0);
973        assert_eq!(call1(z, asin_complex), z.asin());
974        assert_eq!(call1(z, acos_complex), z.acos());
975        assert_eq!(call1(z, atanh_complex), z.atanh());
976        assert_eq!(call1(z, asinh_complex), z.asinh());
977        assert_eq!(call1(z, atan_complex), z.atan());
978        assert_eq!(call1(z, acosh_complex), z.acosh());
979    }
980
981    #[test]
982    fn test_isscalar_zero_d() {
983        use ferray_core::dimension::IxDyn;
984        let scalar: Array<f64, IxDyn> = Array::from_vec(IxDyn::new(&[]), vec![2.5]).unwrap();
985        assert!(isscalar(&scalar));
986
987        let vec: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
988        assert!(!isscalar(&vec));
989    }
990}