Skip to content

Commit 65f0f5a

Browse files
committed
Add phase angle calculation functions for complex arrays
https://en.wikipedia.org/wiki/Argument_(complex_analysis) Implement phase angle (argument) calculation for complex numbers in arrays, providing NumPy-compatible functionality. The angle represents the phase of a complex number in the complex plane, calculated as atan2(imaginary, real). Features: - Calculate phase angles in range (-π, π] for radians, (-180°, 180°] for degrees - Support for real numbers (f32, f64) and complex numbers (Complex<f32>, Complex<f64>) - Two API variants: - NumPy-compatible: always returns f64 regardless of input precision - Precision-preserving: maintains input precision (f32 → f32, f64 → f64) - Both array methods and standalone functions available - Proper handling of signed zeros following NumPy conventions: - angle(+0 + 0i) = +0, angle(-0 + 0i) = +π - angle(+0 - 0i) = -0, angle(-0 - 0i) = -π API additions: - ArrayRef::angle(deg: bool) -> Array<f64, D> - ArrayRef::angle_preserve(deg: bool) -> Array<InputType, D> - ndarray::angle(array, deg) -> Array<f64, D> - ndarray::angle_preserve(array, deg) -> Array<InputType, D> - ndarray::angle_scalar(value, deg) -> f64 All functions tested, feature-gated with std and include documentation.
1 parent 0e1d04b commit 65f0f5a

File tree

3 files changed

+335
-0
lines changed

3 files changed

+335
-0
lines changed

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,8 @@ mod impl_2d;
18491849
mod impl_dyn;
18501850

18511851
mod numeric;
1852+
#[cfg(feature = "std")]
1853+
pub use crate::numeric::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64};
18521854

18531855
pub mod linalg;
18541856

src/numeric/impl_float_maths.rs

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,86 @@
22

33
#[cfg(feature = "std")]
44
use num_traits::Float;
5+
#[cfg(feature = "std")]
6+
use num_traits::FloatConst;
7+
#[cfg(feature = "std")]
8+
use num_complex::Complex;
59

610
use crate::imp_prelude::*;
711

12+
/// Trait for values with a meaningful complex argument (phase).
13+
/// This `*_64` version standardises the *output* to `f64`.
14+
#[cfg(feature = "std")]
15+
pub trait HasAngle64 {
16+
/// Return the phase angle (argument) in radians in the range (-π, π].
17+
fn to_angle64(&self) -> f64;
18+
}
19+
20+
#[cfg(feature = "std")]
21+
impl HasAngle64 for f64 {
22+
#[inline]
23+
fn to_angle64(&self) -> f64 {
24+
(0.0f64).atan2(*self)
25+
}
26+
}
27+
28+
#[cfg(feature = "std")]
29+
impl HasAngle64 for f32 {
30+
#[inline]
31+
fn to_angle64(&self) -> f64 {
32+
// Promote to f64
33+
(0.0f64).atan2(*self as f64)
34+
}
35+
}
36+
37+
#[cfg(feature = "std")]
38+
impl HasAngle64 for Complex<f64> {
39+
#[inline]
40+
fn to_angle64(&self) -> f64 {
41+
self.im.atan2(self.re)
42+
}
43+
}
44+
45+
#[cfg(feature = "std")]
46+
impl HasAngle64 for Complex<f32> {
47+
#[inline]
48+
fn to_angle64(&self) -> f64 {
49+
(self.im as f64).atan2(self.re as f64)
50+
}
51+
}
52+
53+
/// Optional: precision-preserving variant (returns `F`), if you want
54+
/// an API that keeps `f32` outputs for `f32` inputs.
55+
///
56+
/// - Works for `f32`/`f64` and `Complex<f32>`/`Complex<f64>`.
57+
#[cfg(feature = "std")]
58+
pub trait HasAngle<F: Float + FloatConst> {
59+
/// Return the phase angle (argument) in the same precision as the input type.
60+
fn to_angle(&self) -> F;
61+
}
62+
63+
#[cfg(feature = "std")]
64+
impl<F> HasAngle<F> for F
65+
where
66+
F: Float + FloatConst,
67+
{
68+
#[inline]
69+
fn to_angle(&self) -> F {
70+
F::zero().atan2(*self)
71+
}
72+
}
73+
74+
#[cfg(feature = "std")]
75+
impl<F> HasAngle<F> for Complex<F>
76+
where
77+
F: Float + FloatConst,
78+
{
79+
#[inline]
80+
fn to_angle(&self) -> F {
81+
self.im.atan2(self.re)
82+
}
83+
}
84+
885
#[cfg(feature = "std")]
986
macro_rules! boolean_ops {
1087
($(#[$meta1:meta])* fn $func:ident
@@ -167,6 +244,112 @@ where
167244
}
168245
}
169246

247+
/// # Angle calculation methods for arrays
248+
///
249+
/// Methods for calculating phase angles of complex values in arrays.
250+
#[cfg(feature = "std")]
251+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
252+
impl<A, D> ArrayRef<A, D>
253+
where
254+
A: HasAngle64,
255+
D: Dimension,
256+
{
257+
/// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in the array.
258+
///
259+
/// This function always returns `f64` values, regardless of input precision.
260+
/// The angles are returned in the range (-π, π].
261+
///
262+
/// # Arguments
263+
///
264+
/// * `deg` - If `true`, convert radians to degrees; if `false`, return radians.
265+
///
266+
/// # Examples
267+
///
268+
/// ```
269+
/// use ndarray::array;
270+
/// use num_complex::Complex;
271+
/// use std::f64::consts::PI;
272+
///
273+
/// // Real numbers
274+
/// let real_arr = array![1.0, -1.0, 0.0];
275+
/// let angles_rad = real_arr.angle(false);
276+
/// let angles_deg = real_arr.angle(true);
277+
/// assert!((angles_rad[0] - 0.0).abs() < 1e-10);
278+
/// assert!((angles_rad[1] - PI).abs() < 1e-10);
279+
/// assert!((angles_deg[1] - 180.0).abs() < 1e-10);
280+
///
281+
/// // Complex numbers
282+
/// let complex_arr = array![
283+
/// Complex::new(1.0, 0.0),
284+
/// Complex::new(0.0, 1.0),
285+
/// Complex::new(1.0, 1.0),
286+
/// ];
287+
/// let angles = complex_arr.angle(false);
288+
/// assert!((angles[0] - 0.0).abs() < 1e-10);
289+
/// assert!((angles[1] - PI/2.0).abs() < 1e-10);
290+
/// assert!((angles[2] - PI/4.0).abs() < 1e-10);
291+
/// ```
292+
#[must_use = "method returns a new array and does not mutate the original value"]
293+
pub fn angle(&self, deg: bool) -> Array<f64, D>
294+
{
295+
let mut result = self.map(|x| x.to_angle64());
296+
if deg {
297+
result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI);
298+
}
299+
result
300+
}
301+
}
302+
303+
/// # Precision-preserving angle calculation methods
304+
///
305+
/// Methods for calculating phase angles that preserve input precision.
306+
#[cfg(feature = "std")]
307+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
308+
impl<A, D> ArrayRef<A, D>
309+
where
310+
D: Dimension,
311+
{
312+
/// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of values, preserving input precision.
313+
///
314+
/// This method preserves the precision of the input:
315+
/// - `f32` and `Complex<f32>` inputs produce `f32` outputs
316+
/// - `f64` and `Complex<f64>` inputs produce `f64` outputs
317+
///
318+
/// # Arguments
319+
///
320+
/// * `deg` - If `true`, convert radians to degrees; if `false`, return radians.
321+
///
322+
/// # Examples
323+
///
324+
/// ```
325+
/// use ndarray::array;
326+
/// use num_complex::Complex;
327+
///
328+
/// // f32 precision preserved for complex numbers
329+
/// let complex_f32 = array![Complex::new(1.0f32, 1.0f32)];
330+
/// let angles_f32 = complex_f32.angle_preserve(false);
331+
/// // angles_f32 has type Array<f32, _>
332+
///
333+
/// // f64 precision preserved for complex numbers
334+
/// let complex_f64 = array![Complex::new(1.0f64, 1.0f64)];
335+
/// let angles_f64 = complex_f64.angle_preserve(false);
336+
/// // angles_f64 has type Array<f64, _>
337+
/// ```
338+
#[must_use = "method returns a new array and does not mutate the original value"]
339+
pub fn angle_preserve<F>(&self, deg: bool) -> Array<F, D>
340+
where
341+
A: HasAngle<F>,
342+
F: Float + FloatConst,
343+
{
344+
let mut result = self.map(|x| x.to_angle());
345+
if deg {
346+
let factor = F::from(180.0).unwrap() / F::PI();
347+
result.mapv_inplace(|a| a * factor);
348+
}
349+
result
350+
}
351+
}
352+
170353
impl<A, D> ArrayRef<A, D>
171354
where
172355
A: 'static + PartialOrd + Clone,
@@ -191,3 +374,150 @@ where
191374
self.mapv(|a| num_traits::clamp(a, min.clone(), max.clone()))
192375
}
193376
}
377+
378+
/// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in an array.
379+
///
380+
///
381+
/// Always returns `f64`, regardless of input precision.
382+
///
383+
/// # Arguments
384+
///
385+
/// * `z` - Array of real or complex values (f32/f64, `Complex<f32>`/`Complex<f64>`).
386+
/// * `deg` - If `true`, convert radians to degrees.
387+
///
388+
/// # Returns
389+
///
390+
/// An `Array<f64, D>` with the same shape as `z` containing the phase angles.
391+
/// Angles are in the range (-π, π] for radians, or (-180, 180] for degrees.
392+
///
393+
/// # Examples
394+
///
395+
/// ```
396+
/// use ndarray::array;
397+
/// use num_complex::Complex;
398+
/// use std::f64::consts::PI;
399+
///
400+
/// // Real numbers
401+
/// let real_vals = array![1.0, -1.0, 0.0];
402+
/// let angles_rad = ndarray::angle(&real_vals, false);
403+
/// let angles_deg = ndarray::angle(&real_vals, true);
404+
/// assert!((angles_rad[0] - 0.0).abs() < 1e-10);
405+
/// assert!((angles_rad[1] - PI).abs() < 1e-10);
406+
/// assert!((angles_deg[1] - 180.0).abs() < 1e-10);
407+
///
408+
/// // Complex numbers
409+
/// let complex_vals = array![
410+
/// Complex::new(1.0, 0.0),
411+
/// Complex::new(0.0, 1.0),
412+
/// Complex::new(1.0, 1.0),
413+
/// ];
414+
/// let angles = ndarray::angle(&complex_vals, false);
415+
/// assert!((angles[0] - 0.0).abs() < 1e-10);
416+
/// assert!((angles[1] - PI/2.0).abs() < 1e-10);
417+
/// assert!((angles[2] - PI/4.0).abs() < 1e-10);
418+
/// ```
419+
///
420+
/// # Zero handling
421+
///
422+
/// The function follows NumPy's convention for handling zeros:
423+
/// - `+0 + 0i` → `+0`
424+
/// - `-0 + 0i` → `+π`
425+
/// - `+0 - 0i` → `-0`
426+
/// - `-0 - 0i` → `-π`
427+
#[cfg(feature = "std")]
428+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
429+
pub fn angle<T, S, D>(z: &ArrayBase<S, D>, deg: bool) -> Array<f64, D>
430+
where
431+
T: HasAngle64,
432+
S: Data<Elem = T>,
433+
D: Dimension,
434+
{
435+
let mut result = z.map(|x| x.to_angle64());
436+
if deg {
437+
result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI);
438+
}
439+
result
440+
}
441+
442+
/// Scalar convenience function for angle calculation.
443+
///
444+
/// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of a single complex value.
445+
///
446+
/// # Arguments
447+
///
448+
/// * `z` - A real or complex value (f32/f64, `Complex<f32>`/`Complex<f64>`).
449+
/// * `deg` - If `true`, convert radians to degrees.
450+
///
451+
/// # Returns
452+
///
453+
/// The phase angle as `f64` in radians or degrees.
454+
///
455+
/// # Examples
456+
///
457+
/// ```
458+
/// use num_complex::Complex;
459+
/// use std::f64::consts::PI;
460+
///
461+
/// assert!((ndarray::angle_scalar(Complex::new(1.0, 1.0), false) - PI/4.0).abs() < 1e-10);
462+
/// assert!((ndarray::angle_scalar(1.0f32, true) - 0.0).abs() < 1e-10);
463+
/// assert!((ndarray::angle_scalar(-1.0, true) - 180.0).abs() < 1e-10);
464+
/// ```
465+
#[cfg(feature = "std")]
466+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
467+
pub fn angle_scalar<T: HasAngle64>(z: T, deg: bool) -> f64
468+
{
469+
let mut a = z.to_angle64();
470+
if deg {
471+
a *= 180.0f64 / std::f64::consts::PI;
472+
}
473+
a
474+
}
475+
476+
/// Precision-preserving angle calculation function.
477+
///
478+
/// Calculate the phase angle of complex values while preserving input precision.
479+
/// Unlike [`angle`], this function returns the same precision as the input:
480+
/// - `f32` and `Complex<f32>` inputs produce `f32` outputs
481+
/// - `f64` and `Complex<f64>` inputs produce `f64` outputs
482+
///
483+
/// # Arguments
484+
///
485+
/// * `z` - Array of real or complex values.
486+
/// * `deg` - If `true`, convert radians to degrees.
487+
///
488+
/// # Returns
489+
///
490+
/// An `Array<F, D>` with the same shape as `z` and precision matching the input.
491+
///
492+
/// # Examples
493+
///
494+
/// ```
495+
/// use ndarray::array;
496+
/// use num_complex::Complex;
497+
///
498+
/// // f32 precision preserved for complex numbers
499+
/// let z32 = array![Complex::new(0.0f32, 1.0)];
500+
/// let out32 = ndarray::angle_preserve(&z32, false);
501+
/// // out32 has type Array<f32, _>
502+
///
503+
/// // f64 precision preserved for complex numbers
504+
/// let z64 = array![Complex::new(0.0f64, -1.0)];
505+
/// let out64 = ndarray::angle_preserve(&z64, false);
506+
/// // out64 has type Array<f64, _>
507+
/// ```
508+
#[cfg(feature = "std")]
509+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
510+
pub fn angle_preserve<A, F, S, D>(z: &ArrayBase<S, D>, deg: bool) -> Array<F, D>
511+
where
512+
A: HasAngle<F>,
513+
F: Float + FloatConst,
514+
S: Data<Elem = A>,
515+
D: Dimension,
516+
{
517+
let mut result = z.map(|x| x.to_angle());
518+
if deg {
519+
let factor = F::from(180.0).unwrap() / F::PI();
520+
result.mapv_inplace(|a| a * factor);
521+
}
522+
result
523+
}

src/numeric/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
mod impl_numeric;
22

33
mod impl_float_maths;
4+
5+
#[cfg(feature = "std")]
6+
pub use self::impl_float_maths::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64};

0 commit comments

Comments
 (0)