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
5use ferray_core::Array;
6use ferray_core::dimension::Dimension;
7use ferray_core::dtype::Element;
8use ferray_core::error::FerrayResult;
9use num_complex::Complex;
10use num_traits::Float;
11
12/// Extract the real part of a complex array.
13///
14/// Works with `Complex<f32>` and `Complex<f64>` arrays.
15pub fn real<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
16where
17    T: Element + Float,
18    Complex<T>: Element,
19    D: Dimension,
20{
21    let data: Vec<T> = input.iter().map(|c| c.re).collect();
22    Array::from_vec(input.dim().clone(), data)
23}
24
25/// Extract the imaginary part of a complex array.
26pub fn imag<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
27where
28    T: Element + Float,
29    Complex<T>: Element,
30    D: Dimension,
31{
32    let data: Vec<T> = input.iter().map(|c| c.im).collect();
33    Array::from_vec(input.dim().clone(), data)
34}
35
36/// Compute the complex conjugate.
37pub fn conj<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
38where
39    T: Element + Float,
40    Complex<T>: Element,
41    D: Dimension,
42{
43    let data: Vec<Complex<T>> = input.iter().map(num_complex::Complex::conj).collect();
44    Array::from_vec(input.dim().clone(), data)
45}
46
47/// Alias for [`conj`].
48pub fn conjugate<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
49where
50    T: Element + Float,
51    Complex<T>: Element,
52    D: Dimension,
53{
54    conj(input)
55}
56
57/// Compute the angle (argument/phase) of complex numbers.
58///
59/// Returns values in radians, in the range [-pi, pi].
60pub fn angle<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
61where
62    T: Element + Float,
63    Complex<T>: Element,
64    D: Dimension,
65{
66    let data: Vec<T> = input.iter().map(|c| c.im.atan2(c.re)).collect();
67    Array::from_vec(input.dim().clone(), data)
68}
69
70/// Compute the absolute value (magnitude) of complex numbers.
71///
72/// Returns a real array.
73pub fn abs<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<T, D>>
74where
75    T: Element + Float,
76    Complex<T>: Element,
77    D: Dimension,
78{
79    let data: Vec<T> = input
80        .iter()
81        .map(|c| (c.re * c.re + c.im * c.im).sqrt())
82        .collect();
83    Array::from_vec(input.dim().clone(), data)
84}
85
86// ---------------------------------------------------------------------------
87// Complex predicates: iscomplex / isreal / iscomplexobj / isrealobj / isscalar
88// ---------------------------------------------------------------------------
89
90/// Element-wise: true where the imaginary part is non-zero.
91///
92/// Analogous to `numpy.iscomplex(x)`. NumPy returns `False` everywhere for
93/// real-dtype inputs; in Rust, real-dtype arrays are statically distinct
94/// types, so call [`iscomplex_real`] instead for that case.
95pub fn iscomplex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<bool, D>>
96where
97    T: Element + Float,
98    Complex<T>: Element,
99    D: Dimension,
100{
101    let zero = <T as num_traits::Zero>::zero();
102    let data: Vec<bool> = input.iter().map(|c| c.im != zero).collect();
103    Array::from_vec(input.dim().clone(), data)
104}
105
106/// Real-input variant of [`iscomplex`]: always returns `false` everywhere.
107///
108/// Provided so generic code can ask "is this element complex-valued" against
109/// a real-dtype array without a separate type branch. Matches NumPy's
110/// `iscomplex` behavior on real inputs.
111pub fn iscomplex_real<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
112where
113    T: Element,
114    D: Dimension,
115{
116    let data = vec![false; input.size()];
117    Array::from_vec(input.dim().clone(), data)
118}
119
120/// Element-wise: true where the imaginary part is zero.
121///
122/// Analogous to `numpy.isreal(x)` for complex inputs. For real-dtype arrays
123/// see [`isreal_real`] (always-true).
124pub fn isreal<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<bool, D>>
125where
126    T: Element + Float,
127    Complex<T>: Element,
128    D: Dimension,
129{
130    let zero = <T as num_traits::Zero>::zero();
131    let data: Vec<bool> = input.iter().map(|c| c.im == zero).collect();
132    Array::from_vec(input.dim().clone(), data)
133}
134
135/// Real-input variant of [`isreal`]: always returns `true` everywhere.
136pub fn isreal_real<T, D>(input: &Array<T, D>) -> FerrayResult<Array<bool, D>>
137where
138    T: Element,
139    D: Dimension,
140{
141    let data = vec![true; input.size()];
142    Array::from_vec(input.dim().clone(), data)
143}
144
145/// Whether the array's element dtype is a complex type.
146///
147/// Analogous to `numpy.iscomplexobj(x)` — returns a single `bool` based on
148/// the dtype, not a per-element check.
149pub fn iscomplexobj<T, D>(_: &Array<T, D>) -> bool
150where
151    T: Element,
152    D: Dimension,
153{
154    T::dtype().is_complex()
155}
156
157/// Whether the array's element dtype is a real (non-complex) type.
158///
159/// Analogous to `numpy.isrealobj(x)`. Returns `!iscomplexobj(x)`.
160pub fn isrealobj<T, D>(a: &Array<T, D>) -> bool
161where
162    T: Element,
163    D: Dimension,
164{
165    !iscomplexobj(a)
166}
167
168/// Whether the array represents a scalar (0-D).
169///
170/// Analogous to `numpy.isscalar(x)`. In Rust, dimensionality is statically
171/// typed, so this returns `true` only for 0-D arrays (`Ix0` or `IxDyn` with
172/// empty shape).
173pub fn isscalar<T, D>(a: &Array<T, D>) -> bool
174where
175    T: Element,
176    D: Dimension,
177{
178    a.shape().is_empty()
179}
180
181// ---------------------------------------------------------------------------
182// Complex transcendentals — sin/cos/tan, sinh/cosh/tanh, exp/log/sqrt,
183// arc* / arch* and friends. NumPy provides these for complex dtypes via
184// loops_unary_complex.dispatch.c.src; ferray's existing real-only ufuncs
185// (T: Float bound) exclude Complex<f32>/<f64>, so we ship them in this
186// module and re-export at the crate root.
187//
188// All implementations delegate to the corresponding `Complex::*` method
189// from num_complex, which uses well-known identities (e.g. exp(z) =
190// exp(z.re) * (cos(z.im) + i*sin(z.im))).
191// ---------------------------------------------------------------------------
192
193macro_rules! complex_unary {
194    ($fn_name:ident, $method:ident, $doc:expr) => {
195        #[doc = $doc]
196        pub fn $fn_name<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
197        where
198            T: Element + Float,
199            Complex<T>: Element,
200            D: Dimension,
201        {
202            let data: Vec<Complex<T>> = input.iter().map(|z| z.$method()).collect();
203            Array::from_vec(input.dim().clone(), data)
204        }
205    };
206}
207
208complex_unary!(
209    sin_complex,
210    sin,
211    "Element-wise complex sin: sin(z) = sin(z.re)cosh(z.im) + i cos(z.re)sinh(z.im)."
212);
213complex_unary!(cos_complex, cos, "Element-wise complex cos.");
214complex_unary!(tan_complex, tan, "Element-wise complex tan.");
215complex_unary!(sinh_complex, sinh, "Element-wise complex sinh.");
216complex_unary!(cosh_complex, cosh, "Element-wise complex cosh.");
217complex_unary!(tanh_complex, tanh, "Element-wise complex tanh.");
218complex_unary!(asin_complex, asin, "Element-wise complex arcsin.");
219complex_unary!(acos_complex, acos, "Element-wise complex arccos.");
220complex_unary!(atan_complex, atan, "Element-wise complex arctan.");
221complex_unary!(asinh_complex, asinh, "Element-wise complex arcsinh.");
222complex_unary!(acosh_complex, acosh, "Element-wise complex arccosh.");
223complex_unary!(atanh_complex, atanh, "Element-wise complex arctanh.");
224complex_unary!(
225    exp_complex,
226    exp,
227    "Element-wise complex exp: exp(z) = exp(z.re) (cos(z.im) + i sin(z.im))."
228);
229complex_unary!(
230    ln_complex,
231    ln,
232    "Element-wise complex natural log: log(z) = log|z| + i arg(z), branch cut along negative real axis."
233);
234complex_unary!(
235    sqrt_complex,
236    sqrt,
237    "Element-wise complex sqrt; principal branch (Re(sqrt(z)) >= 0)."
238);
239
240/// Element-wise complex log2: ln(z) / ln(2).
241pub fn log2_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
242where
243    T: Element + Float,
244    Complex<T>: Element,
245    D: Dimension,
246{
247    let one = <T as num_traits::One>::one();
248    let ln2: T = <T as num_traits::NumCast>::from(core::f64::consts::LN_2).unwrap();
249    let inv_ln2 = one / ln2;
250    let data: Vec<Complex<T>> = input
251        .iter()
252        .map(|z| {
253            let l = z.ln();
254            Complex::new(l.re * inv_ln2, l.im * inv_ln2)
255        })
256        .collect();
257    Array::from_vec(input.dim().clone(), data)
258}
259
260/// Element-wise complex log10: ln(z) / ln(10).
261pub fn log10_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
262where
263    T: Element + Float,
264    Complex<T>: Element,
265    D: Dimension,
266{
267    let one = <T as num_traits::One>::one();
268    let ln10: T = <T as num_traits::NumCast>::from(core::f64::consts::LN_10).unwrap();
269    let inv_ln10 = one / ln10;
270    let data: Vec<Complex<T>> = input
271        .iter()
272        .map(|z| {
273            let l = z.ln();
274            Complex::new(l.re * inv_ln10, l.im * inv_ln10)
275        })
276        .collect();
277    Array::from_vec(input.dim().clone(), data)
278}
279
280/// Element-wise complex `exp(z) - 1`. For small |z|, naive `exp(z)-1`
281/// suffers catastrophic cancellation; this routine uses the identity
282/// `expm1(z) = expm1(z.re) cos(z.im) + (1+expm1(z.re)) (cos(z.im)-1) + i exp(z.re) sin(z.im)`
283/// rearranged to preserve precision near 0.
284pub fn expm1_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
285where
286    T: Element + Float,
287    Complex<T>: Element,
288    D: Dimension,
289{
290    let one = <T as num_traits::One>::one();
291    let two = one + one;
292    let half: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
293    let data: Vec<Complex<T>> = input
294        .iter()
295        .map(|z| {
296            // exp(z) - 1 = (exp(re) cos(im) - 1) + i exp(re) sin(im)
297            //            = (exp(re)-1) cos(im) + (cos(im)-1) + i exp(re) sin(im)
298            let er_m1 = z.re.exp_m1();
299            let er = er_m1 + one;
300            let cos_im = z.im.cos();
301            let sin_im = z.im.sin();
302            // Use cos(im) - 1 = -2 sin²(im/2) for precision near 0.
303            let s = (z.im * half).sin();
304            let cos_im_m1 = -two * s * s;
305            Complex::new(er_m1 * cos_im + cos_im_m1, er * sin_im)
306        })
307        .collect();
308    Array::from_vec(input.dim().clone(), data)
309}
310
311/// Element-wise complex `log(1 + z)`. Reduces to the equivalent of
312/// `log1p` on the real axis but uses `Complex::ln(1 + z)` directly for
313/// off-axis values; precision near z=0 is preserved by routing through
314/// the identity `log1p(z) = log1p(re) + i atan2(im, 1+re)` when |z| is
315/// small, otherwise falls back to ln(1+z).
316pub fn log1p_complex<T, D>(input: &Array<Complex<T>, D>) -> FerrayResult<Array<Complex<T>, D>>
317where
318    T: Element + Float,
319    Complex<T>: Element,
320    D: Dimension,
321{
322    let one = <T as num_traits::One>::one();
323    let zero = <T as num_traits::Zero>::zero();
324    let two = one + one;
325    let half: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
326    let small: T = <T as num_traits::NumCast>::from(0.5_f64).unwrap();
327    let data: Vec<Complex<T>> = input
328        .iter()
329        .map(|z| {
330            if z.re.abs() < small && z.im.abs() < small {
331                // log(1+z) where |z| small: Re = 0.5*ln((1+re)^2 + im^2);
332                // expand (1+re)^2 + im^2 - 1 = 2 re + re^2 + im^2 = re*(2+re) + im^2.
333                let u = z.re * (two + z.re) + z.im * z.im;
334                let half_log = half * u.ln_1p();
335                let arg = z.im.atan2(one + z.re);
336                Complex::new(half_log, arg)
337            } else {
338                (Complex::new(one, zero) + *z).ln()
339            }
340        })
341        .collect();
342    Array::from_vec(input.dim().clone(), data)
343}
344
345/// Element-wise complex power `z^w` with broadcasting against a scalar
346/// exponent `w`. For complex z and complex w: `z^w = exp(w * log(z))`.
347pub fn power_complex<T, D>(
348    base: &Array<Complex<T>, D>,
349    exponent: Complex<T>,
350) -> FerrayResult<Array<Complex<T>, D>>
351where
352    T: Element + Float,
353    Complex<T>: Element,
354    D: Dimension,
355{
356    let data: Vec<Complex<T>> = base.iter().map(|z| z.powc(exponent)).collect();
357    Array::from_vec(base.dim().clone(), data)
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use ferray_core::dimension::Ix1;
364    use num_complex::Complex64;
365
366    fn arr1_c64(data: Vec<Complex64>) -> Array<Complex64, Ix1> {
367        let n = data.len();
368        Array::from_vec(Ix1::new([n]), data).unwrap()
369    }
370
371    #[test]
372    fn test_real() {
373        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)]);
374        let r = real(&a).unwrap();
375        assert_eq!(r.as_slice().unwrap(), &[1.0, 3.0]);
376    }
377
378    #[test]
379    fn test_imag() {
380        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)]);
381        let r = imag(&a).unwrap();
382        assert_eq!(r.as_slice().unwrap(), &[2.0, 4.0]);
383    }
384
385    #[test]
386    fn test_conj() {
387        let a = arr1_c64(vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, -4.0)]);
388        let r = conj(&a).unwrap();
389        let s = r.as_slice().unwrap();
390        assert_eq!(s[0], Complex64::new(1.0, -2.0));
391        assert_eq!(s[1], Complex64::new(3.0, 4.0));
392    }
393
394    #[test]
395    fn test_conjugate_alias() {
396        let a = arr1_c64(vec![Complex64::new(1.0, 2.0)]);
397        let r = conjugate(&a).unwrap();
398        assert_eq!(r.as_slice().unwrap()[0], Complex64::new(1.0, -2.0));
399    }
400
401    #[test]
402    fn test_angle() {
403        let a = arr1_c64(vec![
404            Complex64::new(1.0, 0.0),
405            Complex64::new(0.0, 1.0),
406            Complex64::new(-1.0, 0.0),
407        ]);
408        let r = angle(&a).unwrap();
409        let s = r.as_slice().unwrap();
410        assert!((s[0] - 0.0).abs() < 1e-12);
411        assert!((s[1] - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
412        assert!((s[2] - std::f64::consts::PI).abs() < 1e-12);
413    }
414
415    #[test]
416    fn test_abs() {
417        let a = arr1_c64(vec![Complex64::new(3.0, 4.0), Complex64::new(0.0, 1.0)]);
418        let r = abs(&a).unwrap();
419        let s = r.as_slice().unwrap();
420        assert!((s[0] - 5.0).abs() < 1e-12);
421        assert!((s[1] - 1.0).abs() < 1e-12);
422    }
423
424    #[test]
425    fn test_iscomplex_complex_input() {
426        let a = arr1_c64(vec![
427            Complex64::new(1.0, 0.0),
428            Complex64::new(2.0, 1.5),
429            Complex64::new(0.0, 0.0),
430        ]);
431        let r = iscomplex(&a).unwrap();
432        assert_eq!(r.as_slice().unwrap(), &[false, true, false]);
433    }
434
435    #[test]
436    fn test_isreal_complex_input() {
437        let a = arr1_c64(vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 1.5)]);
438        let r = isreal(&a).unwrap();
439        assert_eq!(r.as_slice().unwrap(), &[true, false]);
440    }
441
442    #[test]
443    fn test_iscomplex_real_input() {
444        let a: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
445        let r = iscomplex_real(&a).unwrap();
446        assert_eq!(r.as_slice().unwrap(), &[false, false, false]);
447    }
448
449    #[test]
450    fn test_isreal_real_input() {
451        let a: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
452        let r = isreal_real(&a).unwrap();
453        assert_eq!(r.as_slice().unwrap(), &[true, true, true]);
454    }
455
456    #[test]
457    fn test_iscomplexobj_isrealobj() {
458        let c = arr1_c64(vec![Complex64::new(1.0, 2.0)]);
459        assert!(iscomplexobj(&c));
460        assert!(!isrealobj(&c));
461
462        let r: Array<f64, Ix1> = Array::from_vec(Ix1::new([2]), vec![1.0, 2.0]).unwrap();
463        assert!(!iscomplexobj(&r));
464        assert!(isrealobj(&r));
465    }
466
467    fn approx_eq_c(a: Complex64, b: Complex64, eps: f64) -> bool {
468        (a.re - b.re).abs() <= eps && (a.im - b.im).abs() <= eps
469    }
470
471    #[test]
472    fn test_sin_complex_matches_identity() {
473        // sin(i) = i sinh(1) ≈ i*1.1752011936438014
474        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0), Complex64::new(1.0, 1.0)]);
475        let out = sin_complex(&arr).unwrap();
476        let v0 = out.iter().next().copied().unwrap();
477        assert!(approx_eq_c(
478            v0,
479            Complex64::new(0.0, 1.1752011936438014),
480            1e-10
481        ));
482    }
483
484    #[test]
485    fn test_cos_complex_matches_identity() {
486        // cos(i) = cosh(1) ≈ 1.5430806348152437
487        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0)]);
488        let out = cos_complex(&arr).unwrap();
489        let v0 = out.iter().next().copied().unwrap();
490        assert!(approx_eq_c(
491            v0,
492            Complex64::new(1.5430806348152437, 0.0),
493            1e-10
494        ));
495    }
496
497    #[test]
498    fn test_exp_complex_eulers_identity() {
499        // e^{i*pi} = -1 (Euler's identity).
500        let arr = arr1_c64(vec![Complex64::new(0.0, std::f64::consts::PI)]);
501        let out = exp_complex(&arr).unwrap();
502        let v0 = out.iter().next().copied().unwrap();
503        assert!(approx_eq_c(v0, Complex64::new(-1.0, 0.0), 1e-12));
504    }
505
506    #[test]
507    fn test_ln_complex_inverse_of_exp() {
508        let z = Complex64::new(0.7, 1.3);
509        let arr = arr1_c64(vec![z.exp()]);
510        let out = ln_complex(&arr).unwrap();
511        let v = out.iter().next().copied().unwrap();
512        assert!(approx_eq_c(v, z, 1e-12));
513    }
514
515    #[test]
516    fn test_sqrt_complex_principal_branch() {
517        // sqrt(i) = (1+i)/sqrt(2)
518        let arr = arr1_c64(vec![Complex64::new(0.0, 1.0)]);
519        let out = sqrt_complex(&arr).unwrap();
520        let v = out.iter().next().copied().unwrap();
521        let inv_sqrt2 = 1.0_f64 / 2.0_f64.sqrt();
522        assert!(approx_eq_c(v, Complex64::new(inv_sqrt2, inv_sqrt2), 1e-12));
523    }
524
525    #[test]
526    fn test_log2_complex() {
527        // log2(2 + 0i) = 1
528        let arr = arr1_c64(vec![Complex64::new(2.0, 0.0)]);
529        let out = log2_complex(&arr).unwrap();
530        let v = out.iter().next().copied().unwrap();
531        assert!(approx_eq_c(v, Complex64::new(1.0, 0.0), 1e-12));
532    }
533
534    #[test]
535    fn test_log10_complex() {
536        // log10(100 + 0i) = 2
537        let arr = arr1_c64(vec![Complex64::new(100.0, 0.0)]);
538        let out = log10_complex(&arr).unwrap();
539        let v = out.iter().next().copied().unwrap();
540        assert!(approx_eq_c(v, Complex64::new(2.0, 0.0), 1e-12));
541    }
542
543    #[test]
544    fn test_expm1_complex_small_arg_precision() {
545        // For very tiny z, expm1(z) ≈ z + z²/2 + ..., dominated by z.
546        // The naive `z.exp() - 1` suffers catastrophic cancellation here.
547        // Our implementation routes through exp_m1 + sin² identity to
548        // preserve the leading Taylor term. Tolerance is loose because
549        // the 2nd-order term and rounding both contribute at the
550        // ~1e-12 * f64-eps ≈ 1e-28 scale, which dominates the cos·im_m1
551        // fixup arithmetic.
552        let z = Complex64::new(1e-12, 1e-12);
553        let arr = arr1_c64(vec![z]);
554        let out = expm1_complex(&arr).unwrap();
555        let v = out.iter().next().copied().unwrap();
556        // Result must be close to z (Taylor leading term) within a few
557        // ULPs of the magnitude. f64 ULP for 1e-12 ≈ 2.22e-28.
558        let rel_err = (v - z).norm() / z.norm();
559        assert!(
560            rel_err < 1e-4,
561            "expm1 small-arg rel_err = {rel_err:e}, v = {v:?}"
562        );
563    }
564
565    #[test]
566    fn test_log1p_complex_small_arg_precision() {
567        // log1p(z) for tiny z ≈ z - z²/2 + ..., dominated by z.
568        let z = Complex64::new(1e-10, 1e-10);
569        let arr = arr1_c64(vec![z]);
570        let out = log1p_complex(&arr).unwrap();
571        let v = out.iter().next().copied().unwrap();
572        let rel_err = (v - z).norm() / z.norm();
573        assert!(
574            rel_err < 1e-4,
575            "log1p small-arg rel_err = {rel_err:e}, v = {v:?}"
576        );
577    }
578
579    #[test]
580    fn test_expm1_complex_moderate_arg_matches_naive() {
581        // For moderate-magnitude z, expm1 should agree with z.exp() - 1.
582        let z = Complex64::new(0.5, 0.3);
583        let arr = arr1_c64(vec![z]);
584        let out = expm1_complex(&arr).unwrap();
585        let v = out.iter().next().copied().unwrap();
586        let expected = z.exp() - Complex64::new(1.0, 0.0);
587        assert!(approx_eq_c(v, expected, 1e-12));
588    }
589
590    #[test]
591    fn test_log1p_complex_moderate_arg_matches_naive() {
592        let z = Complex64::new(0.4, 0.2);
593        let arr = arr1_c64(vec![z]);
594        let out = log1p_complex(&arr).unwrap();
595        let v = out.iter().next().copied().unwrap();
596        let expected = (Complex64::new(1.0, 0.0) + z).ln();
597        assert!(approx_eq_c(v, expected, 1e-12));
598    }
599
600    #[test]
601    fn test_power_complex_integer_exponent() {
602        // (1+i)^2 = 2i
603        let arr = arr1_c64(vec![Complex64::new(1.0, 1.0)]);
604        let out = power_complex(&arr, Complex64::new(2.0, 0.0)).unwrap();
605        let v = out.iter().next().copied().unwrap();
606        assert!(approx_eq_c(v, Complex64::new(0.0, 2.0), 1e-12));
607    }
608
609    #[test]
610    fn test_tanh_complex_round_trip() {
611        // atanh(tanh(z)) ≈ z (within principal branch).
612        let z = Complex64::new(0.5, 0.3);
613        let arr = arr1_c64(vec![z]);
614        let t = tanh_complex(&arr).unwrap();
615        let r = atanh_complex(&t).unwrap();
616        let v = r.iter().next().copied().unwrap();
617        assert!(approx_eq_c(v, z, 1e-12));
618    }
619
620    #[test]
621    fn test_isscalar_zero_d() {
622        use ferray_core::dimension::IxDyn;
623        let scalar: Array<f64, IxDyn> = Array::from_vec(IxDyn::new(&[]), vec![2.5]).unwrap();
624        assert!(isscalar(&scalar));
625
626        let vec: Array<f64, Ix1> = Array::from_vec(Ix1::new([3]), vec![1.0, 2.0, 3.0]).unwrap();
627        assert!(!isscalar(&vec));
628    }
629}