From 65f0f5a415550f6858e418deb06f4b25ecd5a44c Mon Sep 17 00:00:00 2001 From: Jack Geraghty Date: Wed, 5 Nov 2025 14:25:31 +0000 Subject: [PATCH 1/3] Add phase angle calculation functions for complex arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, Complex) - 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 - ArrayRef::angle_preserve(deg: bool) -> Array - ndarray::angle(array, deg) -> Array - ndarray::angle_preserve(array, deg) -> Array - ndarray::angle_scalar(value, deg) -> f64 All functions tested, feature-gated with std and include documentation. --- src/lib.rs | 2 + src/numeric/impl_float_maths.rs | 330 ++++++++++++++++++++++++++++++++ src/numeric/mod.rs | 3 + 3 files changed, 335 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index baa62ca5b..a90b223dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1849,6 +1849,8 @@ mod impl_2d; mod impl_dyn; mod numeric; +#[cfg(feature = "std")] +pub use crate::numeric::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64}; pub mod linalg; diff --git a/src/numeric/impl_float_maths.rs b/src/numeric/impl_float_maths.rs index 358d57cf3..20c3acb6f 100644 --- a/src/numeric/impl_float_maths.rs +++ b/src/numeric/impl_float_maths.rs @@ -2,9 +2,86 @@ #[cfg(feature = "std")] use num_traits::Float; +#[cfg(feature = "std")] +use num_traits::FloatConst; +#[cfg(feature = "std")] +use num_complex::Complex; use crate::imp_prelude::*; +/// Trait for values with a meaningful complex argument (phase). +/// This `*_64` version standardises the *output* to `f64`. +#[cfg(feature = "std")] +pub trait HasAngle64 { + /// Return the phase angle (argument) in radians in the range (-π, π]. + fn to_angle64(&self) -> f64; +} + +#[cfg(feature = "std")] +impl HasAngle64 for f64 { + #[inline] + fn to_angle64(&self) -> f64 { + (0.0f64).atan2(*self) + } +} + +#[cfg(feature = "std")] +impl HasAngle64 for f32 { + #[inline] + fn to_angle64(&self) -> f64 { + // Promote to f64 + (0.0f64).atan2(*self as f64) + } +} + +#[cfg(feature = "std")] +impl HasAngle64 for Complex { + #[inline] + fn to_angle64(&self) -> f64 { + self.im.atan2(self.re) + } +} + +#[cfg(feature = "std")] +impl HasAngle64 for Complex { + #[inline] + fn to_angle64(&self) -> f64 { + (self.im as f64).atan2(self.re as f64) + } +} + +/// Optional: precision-preserving variant (returns `F`), if you want +/// an API that keeps `f32` outputs for `f32` inputs. +/// +/// - Works for `f32`/`f64` and `Complex`/`Complex`. +#[cfg(feature = "std")] +pub trait HasAngle { + /// Return the phase angle (argument) in the same precision as the input type. + fn to_angle(&self) -> F; +} + +#[cfg(feature = "std")] +impl HasAngle for F +where + F: Float + FloatConst, +{ + #[inline] + fn to_angle(&self) -> F { + F::zero().atan2(*self) + } +} + +#[cfg(feature = "std")] +impl HasAngle for Complex +where + F: Float + FloatConst, +{ + #[inline] + fn to_angle(&self) -> F { + self.im.atan2(self.re) + } +} + #[cfg(feature = "std")] macro_rules! boolean_ops { ($(#[$meta1:meta])* fn $func:ident @@ -167,6 +244,112 @@ where } } +/// # Angle calculation methods for arrays +/// +/// Methods for calculating phase angles of complex values in arrays. +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl ArrayRef +where + A: HasAngle64, + D: Dimension, +{ + /// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in the array. + /// + /// This function always returns `f64` values, regardless of input precision. + /// The angles are returned in the range (-π, π]. + /// + /// # Arguments + /// + /// * `deg` - If `true`, convert radians to degrees; if `false`, return radians. + /// + /// # Examples + /// + /// ``` + /// use ndarray::array; + /// use num_complex::Complex; + /// use std::f64::consts::PI; + /// + /// // Real numbers + /// let real_arr = array![1.0, -1.0, 0.0]; + /// let angles_rad = real_arr.angle(false); + /// let angles_deg = real_arr.angle(true); + /// assert!((angles_rad[0] - 0.0).abs() < 1e-10); + /// assert!((angles_rad[1] - PI).abs() < 1e-10); + /// assert!((angles_deg[1] - 180.0).abs() < 1e-10); + /// + /// // Complex numbers + /// let complex_arr = array![ + /// Complex::new(1.0, 0.0), + /// Complex::new(0.0, 1.0), + /// Complex::new(1.0, 1.0), + /// ]; + /// let angles = complex_arr.angle(false); + /// assert!((angles[0] - 0.0).abs() < 1e-10); + /// assert!((angles[1] - PI/2.0).abs() < 1e-10); + /// assert!((angles[2] - PI/4.0).abs() < 1e-10); + /// ``` + #[must_use = "method returns a new array and does not mutate the original value"] + pub fn angle(&self, deg: bool) -> Array + { + let mut result = self.map(|x| x.to_angle64()); + if deg { + result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI); + } + result + } +} + +/// # Precision-preserving angle calculation methods +/// +/// Methods for calculating phase angles that preserve input precision. +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl ArrayRef +where + D: Dimension, +{ + /// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of values, preserving input precision. + /// + /// This method preserves the precision of the input: + /// - `f32` and `Complex` inputs produce `f32` outputs + /// - `f64` and `Complex` inputs produce `f64` outputs + /// + /// # Arguments + /// + /// * `deg` - If `true`, convert radians to degrees; if `false`, return radians. + /// + /// # Examples + /// + /// ``` + /// use ndarray::array; + /// use num_complex::Complex; + /// + /// // f32 precision preserved for complex numbers + /// let complex_f32 = array![Complex::new(1.0f32, 1.0f32)]; + /// let angles_f32 = complex_f32.angle_preserve(false); + /// // angles_f32 has type Array + /// + /// // f64 precision preserved for complex numbers + /// let complex_f64 = array![Complex::new(1.0f64, 1.0f64)]; + /// let angles_f64 = complex_f64.angle_preserve(false); + /// // angles_f64 has type Array + /// ``` + #[must_use = "method returns a new array and does not mutate the original value"] + pub fn angle_preserve(&self, deg: bool) -> Array + where + A: HasAngle, + F: Float + FloatConst, + { + let mut result = self.map(|x| x.to_angle()); + if deg { + let factor = F::from(180.0).unwrap() / F::PI(); + result.mapv_inplace(|a| a * factor); + } + result + } +} + impl ArrayRef where A: 'static + PartialOrd + Clone, @@ -191,3 +374,150 @@ where self.mapv(|a| num_traits::clamp(a, min.clone(), max.clone())) } } + +/// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in an array. +/// +/// +/// Always returns `f64`, regardless of input precision. +/// +/// # Arguments +/// +/// * `z` - Array of real or complex values (f32/f64, `Complex`/`Complex`). +/// * `deg` - If `true`, convert radians to degrees. +/// +/// # Returns +/// +/// An `Array` with the same shape as `z` containing the phase angles. +/// Angles are in the range (-π, π] for radians, or (-180, 180] for degrees. +/// +/// # Examples +/// +/// ``` +/// use ndarray::array; +/// use num_complex::Complex; +/// use std::f64::consts::PI; +/// +/// // Real numbers +/// let real_vals = array![1.0, -1.0, 0.0]; +/// let angles_rad = ndarray::angle(&real_vals, false); +/// let angles_deg = ndarray::angle(&real_vals, true); +/// assert!((angles_rad[0] - 0.0).abs() < 1e-10); +/// assert!((angles_rad[1] - PI).abs() < 1e-10); +/// assert!((angles_deg[1] - 180.0).abs() < 1e-10); +/// +/// // Complex numbers +/// let complex_vals = array![ +/// Complex::new(1.0, 0.0), +/// Complex::new(0.0, 1.0), +/// Complex::new(1.0, 1.0), +/// ]; +/// let angles = ndarray::angle(&complex_vals, false); +/// assert!((angles[0] - 0.0).abs() < 1e-10); +/// assert!((angles[1] - PI/2.0).abs() < 1e-10); +/// assert!((angles[2] - PI/4.0).abs() < 1e-10); +/// ``` +/// +/// # Zero handling +/// +/// The function follows NumPy's convention for handling zeros: +/// - `+0 + 0i` → `+0` +/// - `-0 + 0i` → `+π` +/// - `+0 - 0i` → `-0` +/// - `-0 - 0i` → `-π` +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub fn angle(z: &ArrayBase, deg: bool) -> Array +where + T: HasAngle64, + S: Data, + D: Dimension, +{ + let mut result = z.map(|x| x.to_angle64()); + if deg { + result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI); + } + result +} + +/// Scalar convenience function for angle calculation. +/// +/// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of a single complex value. +/// +/// # Arguments +/// +/// * `z` - A real or complex value (f32/f64, `Complex`/`Complex`). +/// * `deg` - If `true`, convert radians to degrees. +/// +/// # Returns +/// +/// The phase angle as `f64` in radians or degrees. +/// +/// # Examples +/// +/// ``` +/// use num_complex::Complex; +/// use std::f64::consts::PI; +/// +/// assert!((ndarray::angle_scalar(Complex::new(1.0, 1.0), false) - PI/4.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(1.0f32, true) - 0.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(-1.0, true) - 180.0).abs() < 1e-10); +/// ``` +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub fn angle_scalar(z: T, deg: bool) -> f64 +{ + let mut a = z.to_angle64(); + if deg { + a *= 180.0f64 / std::f64::consts::PI; + } + a +} + +/// Precision-preserving angle calculation function. +/// +/// Calculate the phase angle of complex values while preserving input precision. +/// Unlike [`angle`], this function returns the same precision as the input: +/// - `f32` and `Complex` inputs produce `f32` outputs +/// - `f64` and `Complex` inputs produce `f64` outputs +/// +/// # Arguments +/// +/// * `z` - Array of real or complex values. +/// * `deg` - If `true`, convert radians to degrees. +/// +/// # Returns +/// +/// An `Array` with the same shape as `z` and precision matching the input. +/// +/// # Examples +/// +/// ``` +/// use ndarray::array; +/// use num_complex::Complex; +/// +/// // f32 precision preserved for complex numbers +/// let z32 = array![Complex::new(0.0f32, 1.0)]; +/// let out32 = ndarray::angle_preserve(&z32, false); +/// // out32 has type Array +/// +/// // f64 precision preserved for complex numbers +/// let z64 = array![Complex::new(0.0f64, -1.0)]; +/// let out64 = ndarray::angle_preserve(&z64, false); +/// // out64 has type Array +/// ``` +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub fn angle_preserve(z: &ArrayBase, deg: bool) -> Array +where + A: HasAngle, + F: Float + FloatConst, + S: Data, + D: Dimension, +{ + let mut result = z.map(|x| x.to_angle()); + if deg { + let factor = F::from(180.0).unwrap() / F::PI(); + result.mapv_inplace(|a| a * factor); + } + result +} diff --git a/src/numeric/mod.rs b/src/numeric/mod.rs index c0a7228c5..8b63747f6 100644 --- a/src/numeric/mod.rs +++ b/src/numeric/mod.rs @@ -1,3 +1,6 @@ mod impl_numeric; mod impl_float_maths; + +#[cfg(feature = "std")] +pub use self::impl_float_maths::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64}; From b46bf2f0a917e3131ffdd17beb037d393ea2b45e Mon Sep 17 00:00:00 2001 From: Jack Geraghty Date: Thu, 6 Nov 2025 16:50:10 +0000 Subject: [PATCH 2/3] Remove always f64 functions and HasAngleF64 trait. --- src/lib.rs | 2 +- src/numeric/impl_float_maths.rs | 339 ++++++++++++++++++++------------ src/numeric/mod.rs | 2 +- 3 files changed, 219 insertions(+), 124 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a90b223dd..dc4f921db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1850,7 +1850,7 @@ mod impl_dyn; mod numeric; #[cfg(feature = "std")] -pub use crate::numeric::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64}; +pub use crate::numeric::{angle, angle_scalar, HasAngle}; pub mod linalg; diff --git a/src/numeric/impl_float_maths.rs b/src/numeric/impl_float_maths.rs index 20c3acb6f..be764052e 100644 --- a/src/numeric/impl_float_maths.rs +++ b/src/numeric/impl_float_maths.rs @@ -1,55 +1,12 @@ // Element-wise methods for ndarray #[cfg(feature = "std")] -use num_traits::Float; -#[cfg(feature = "std")] -use num_traits::FloatConst; +use num_traits::{Float, FloatConst, NumCast}; #[cfg(feature = "std")] use num_complex::Complex; use crate::imp_prelude::*; -/// Trait for values with a meaningful complex argument (phase). -/// This `*_64` version standardises the *output* to `f64`. -#[cfg(feature = "std")] -pub trait HasAngle64 { - /// Return the phase angle (argument) in radians in the range (-π, π]. - fn to_angle64(&self) -> f64; -} - -#[cfg(feature = "std")] -impl HasAngle64 for f64 { - #[inline] - fn to_angle64(&self) -> f64 { - (0.0f64).atan2(*self) - } -} - -#[cfg(feature = "std")] -impl HasAngle64 for f32 { - #[inline] - fn to_angle64(&self) -> f64 { - // Promote to f64 - (0.0f64).atan2(*self as f64) - } -} - -#[cfg(feature = "std")] -impl HasAngle64 for Complex { - #[inline] - fn to_angle64(&self) -> f64 { - self.im.atan2(self.re) - } -} - -#[cfg(feature = "std")] -impl HasAngle64 for Complex { - #[inline] - fn to_angle64(&self) -> f64 { - (self.im as f64).atan2(self.re as f64) - } -} - /// Optional: precision-preserving variant (returns `F`), if you want /// an API that keeps `f32` outputs for `f32` inputs. /// @@ -251,7 +208,6 @@ where #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl ArrayRef where - A: HasAngle64, D: Dimension, { /// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in the array. @@ -271,7 +227,7 @@ where /// use std::f64::consts::PI; /// /// // Real numbers - /// let real_arr = array![1.0, -1.0, 0.0]; + /// let real_arr = array![1.0f64, -1.0, 0.0]; /// let angles_rad = real_arr.angle(false); /// let angles_deg = real_arr.angle(true); /// assert!((angles_rad[0] - 0.0).abs() < 1e-10); @@ -280,7 +236,7 @@ where /// /// // Complex numbers /// let complex_arr = array![ - /// Complex::new(1.0, 0.0), + /// Complex::new(1.0f64, 0.0), /// Complex::new(0.0, 1.0), /// Complex::new(1.0, 1.0), /// ]; @@ -290,11 +246,13 @@ where /// assert!((angles[2] - PI/4.0).abs() < 1e-10); /// ``` #[must_use = "method returns a new array and does not mutate the original value"] - pub fn angle(&self, deg: bool) -> Array + pub fn angle(&self, deg: bool) -> Array + where + A: HasAngle, { - let mut result = self.map(|x| x.to_angle64()); + let mut result = self.map(|x| x.to_angle()); if deg { - result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI); + result.mapv_inplace(|a| a * F::from(180.0).unwrap() / F::PI()); } result } @@ -375,70 +333,6 @@ where } } -/// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in an array. -/// -/// -/// Always returns `f64`, regardless of input precision. -/// -/// # Arguments -/// -/// * `z` - Array of real or complex values (f32/f64, `Complex`/`Complex`). -/// * `deg` - If `true`, convert radians to degrees. -/// -/// # Returns -/// -/// An `Array` with the same shape as `z` containing the phase angles. -/// Angles are in the range (-π, π] for radians, or (-180, 180] for degrees. -/// -/// # Examples -/// -/// ``` -/// use ndarray::array; -/// use num_complex::Complex; -/// use std::f64::consts::PI; -/// -/// // Real numbers -/// let real_vals = array![1.0, -1.0, 0.0]; -/// let angles_rad = ndarray::angle(&real_vals, false); -/// let angles_deg = ndarray::angle(&real_vals, true); -/// assert!((angles_rad[0] - 0.0).abs() < 1e-10); -/// assert!((angles_rad[1] - PI).abs() < 1e-10); -/// assert!((angles_deg[1] - 180.0).abs() < 1e-10); -/// -/// // Complex numbers -/// let complex_vals = array![ -/// Complex::new(1.0, 0.0), -/// Complex::new(0.0, 1.0), -/// Complex::new(1.0, 1.0), -/// ]; -/// let angles = ndarray::angle(&complex_vals, false); -/// assert!((angles[0] - 0.0).abs() < 1e-10); -/// assert!((angles[1] - PI/2.0).abs() < 1e-10); -/// assert!((angles[2] - PI/4.0).abs() < 1e-10); -/// ``` -/// -/// # Zero handling -/// -/// The function follows NumPy's convention for handling zeros: -/// - `+0 + 0i` → `+0` -/// - `-0 + 0i` → `+π` -/// - `+0 - 0i` → `-0` -/// - `-0 - 0i` → `-π` -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -pub fn angle(z: &ArrayBase, deg: bool) -> Array -where - T: HasAngle64, - S: Data, - D: Dimension, -{ - let mut result = z.map(|x| x.to_angle64()); - if deg { - result.mapv_inplace(|a| a * 180.0f64 / std::f64::consts::PI); - } - result -} - /// Scalar convenience function for angle calculation. /// /// Calculate the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of a single complex value. @@ -458,17 +352,18 @@ where /// use num_complex::Complex; /// use std::f64::consts::PI; /// -/// assert!((ndarray::angle_scalar(Complex::new(1.0, 1.0), false) - PI/4.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(Complex::new(1.0f64, 1.0), false) - PI/4.0).abs() < 1e-10); /// assert!((ndarray::angle_scalar(1.0f32, true) - 0.0).abs() < 1e-10); -/// assert!((ndarray::angle_scalar(-1.0, true) - 180.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(-1.0f32, true) - 180.0).abs() < 1e-10); /// ``` #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] -pub fn angle_scalar(z: T, deg: bool) -> f64 +pub fn angle_scalar>(z: T, deg: bool) -> F { - let mut a = z.to_angle64(); + let mut a = z.to_angle(); if deg { - a *= 180.0f64 / std::f64::consts::PI; + + a = a * ::from(180.0).expect("180.0 is a valid f32 and f64 -- this should not fail") / F::PI(); } a } @@ -497,17 +392,17 @@ pub fn angle_scalar(z: T, deg: bool) -> f64 /// /// // f32 precision preserved for complex numbers /// let z32 = array![Complex::new(0.0f32, 1.0)]; -/// let out32 = ndarray::angle_preserve(&z32, false); +/// let out32 = ndarray::angle(&z32, false); /// // out32 has type Array /// /// // f64 precision preserved for complex numbers /// let z64 = array![Complex::new(0.0f64, -1.0)]; -/// let out64 = ndarray::angle_preserve(&z64, false); +/// let out64 = ndarray::angle(&z64, false); /// // out64 has type Array /// ``` #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] -pub fn angle_preserve(z: &ArrayBase, deg: bool) -> Array +pub fn angle(z: &ArrayBase, deg: bool) -> Array where A: HasAngle, F: Float + FloatConst, @@ -521,3 +416,203 @@ where } result } + +#[cfg(all(test, feature = "std"))] +mod angle_tests { + use super::*; + use crate::Array; + use num_complex::Complex; + use std::f64::consts::PI; + + #[test] + fn test_real_numbers_radians() { + let arr = Array::from_vec(vec![1.0f64, -1.0, 0.0]); + let angles = arr.angle(false); + + assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1.0) should be 0"); + assert!((angles[1] - PI).abs() < 1e-10, "angle(-1.0) should be π"); + assert!(angles[2].abs() < 1e-10, "angle(0.0) should be 0"); + } + + #[test] + fn test_real_numbers_degrees() { + let arr = Array::from_vec(vec![1.0f64, -1.0, 0.0]); + let angles = arr.angle(true); + + assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1.0) should be 0°"); + assert!((angles[1] - 180.0).abs() < 1e-10, "angle(-1.0) should be 180°"); + assert!(angles[2].abs() < 1e-10, "angle(0.0) should be 0°"); + } + + #[test] + fn test_complex_numbers_radians() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f64, 0.0), // 0 + Complex::new(0.0, 1.0), // π/2 + Complex::new(-1.0, 0.0), // π + Complex::new(0.0, -1.0), // -π/2 + Complex::new(1.0, 1.0), // π/4 + Complex::new(-1.0, -1.0), // -3π/4 + ]); + let angles = arr.angle(false); + + assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1+0i) should be 0"); + assert!((angles[1] - PI/2.0).abs() < 1e-10, "angle(0+1i) should be π/2"); + assert!((angles[2] - PI).abs() < 1e-10, "angle(-1+0i) should be π"); + assert!((angles[3] - (-PI/2.0)).abs() < 1e-10, "angle(0-1i) should be -π/2"); + assert!((angles[4] - PI/4.0).abs() < 1e-10, "angle(1+1i) should be π/4"); + assert!((angles[5] - (-3.0*PI/4.0)).abs() < 1e-10, "angle(-1-1i) should be -3π/4"); + } + + #[test] + fn test_complex_numbers_degrees() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f64, 0.0), + Complex::new(0.0, 1.0), + Complex::new(-1.0, 0.0), + Complex::new(1.0, 1.0), + ]); + let angles = arr.angle(true); + + assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1+0i) should be 0°"); + assert!((angles[1] - 90.0).abs() < 1e-10, "angle(0+1i) should be 90°"); + assert!((angles[2] - 180.0).abs() < 1e-10, "angle(-1+0i) should be 180°"); + assert!((angles[3] - 45.0).abs() < 1e-10, "angle(1+1i) should be 45°"); + } + + #[test] + fn test_signed_zeros() { + let arr = Array::from_vec(vec![ + Complex::new(0.0f64, 0.0), // +0 + 0i → +0 + Complex::new(-0.0, 0.0), // -0 + 0i → +π + Complex::new(0.0, -0.0), // +0 - 0i → -0 + Complex::new(-0.0, -0.0), // -0 - 0i → -π + ]); + let angles = arr.angle(false); + + assert!(angles[0] >= 0.0 && angles[0].abs() < 1e-10, "+0+0i should give +0"); + assert!((angles[1] - PI).abs() < 1e-10, "-0+0i should give +π"); + assert!(angles[2] <= 0.0 && angles[2].abs() < 1e-10, "+0-0i should give -0"); + assert!((angles[3] - (-PI)).abs() < 1e-10, "-0-0i should give -π"); + } + + #[test] + fn test_angle_preserve_f32() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f32, 1.0), + Complex::new(-1.0, 0.0), + ]); + let angles = arr.angle_preserve(false); + + assert!((angles[0] - std::f32::consts::FRAC_PI_4).abs() < 1e-6); + assert!((angles[1] - std::f32::consts::PI).abs() < 1e-6); + } + + #[test] + fn test_angle_preserve_f64() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f64, 1.0), + Complex::new(-1.0, 0.0), + ]); + let angles = arr.angle_preserve(false); + + assert!((angles[0] - PI/4.0).abs() < 1e-10); + assert!((angles[1] - PI).abs() < 1e-10); + } + + #[test] + fn test_angle_scalar_f64() { + assert!((angle_scalar(Complex::new(1.0f64, 1.0), false) - PI/4.0).abs() < 1e-10); + assert!((angle_scalar(1.0f64, false) - 0.0).abs() < 1e-10); + assert!((angle_scalar(-1.0f64, false) - PI).abs() < 1e-10); + assert!((angle_scalar(-1.0f64, true) - 180.0).abs() < 1e-10); + } + + #[test] + fn test_angle_scalar_f32() { + assert!((angle_scalar(Complex::new(1.0f32, 1.0), false) - std::f32::consts::FRAC_PI_4).abs() < 1e-6); + assert!((angle_scalar(1.0f32, true) - 0.0).abs() < 1e-6); + assert!((angle_scalar(-1.0f32, true) - 180.0).abs() < 1e-6); + } + + #[test] + fn test_angle_function() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f64, 0.0), + Complex::new(0.0, 1.0), + Complex::new(-1.0, 1.0), + ]); + let angles = angle(&arr, false); + + assert!((angles[0] - 0.0).abs() < 1e-10); + assert!((angles[1] - PI/2.0).abs() < 1e-10); + assert!((angles[2] - 3.0*PI/4.0).abs() < 1e-10); + } + + #[test] + fn test_angle_function_degrees() { + let arr = Array::from_vec(vec![ + Complex::new(1.0f32, 1.0), + Complex::new(-1.0, 0.0), + ]); + let angles = angle(&arr, true); + + assert!((angles[0] - 45.0).abs() < 1e-6); + assert!((angles[1] - 180.0).abs() < 1e-6); + } + + #[test] + fn test_edge_cases() { + let arr = Array::from_vec(vec![ + Complex::new(f64::INFINITY, 0.0), + Complex::new(0.0, f64::INFINITY), + Complex::new(f64::NEG_INFINITY, 0.0), + Complex::new(0.0, f64::NEG_INFINITY), + ]); + let angles = arr.angle(false); + + assert!((angles[0] - 0.0).abs() < 1e-10, "angle(∞+0i) should be 0"); + assert!((angles[1] - PI/2.0).abs() < 1e-10, "angle(0+∞i) should be π/2"); + assert!((angles[2] - PI).abs() < 1e-10, "angle(-∞+0i) should be π"); + assert!((angles[3] - (-PI/2.0)).abs() < 1e-10, "angle(0-∞i) should be -π/2"); + } + + #[test] + fn test_mixed_precision() { + // Test that f32 and f64 can be mixed in the same operation + let arr_f32 = Array::from_vec(vec![1.0f32, -1.0f32]); + let angles_f32 = arr_f32.angle(false); + + let arr_f64 = Array::from_vec(vec![1.0f64, -1.0f64]); + let angles_f64 = arr_f64.angle(false); + + // Results should be equivalent within floating point precision + assert!((angles_f32[0] as f64 - angles_f64[0]).abs() < 1e-6); + assert!((angles_f32[1] as f64 - angles_f64[1]).abs() < 1e-6); + } + + #[test] + fn test_range_validation() { + // Generate points on the unit circle and verify angle range + let n = 16; + let mut complex_arr = Vec::new(); + + for i in 0..n { + let theta = 2.0 * PI * (i as f64) / (n as f64); + if theta <= PI { + complex_arr.push(Complex::new(theta.cos(), theta.sin())); + } else { + // For angles > π, we expect negative result in range (-π, 0] + complex_arr.push(Complex::new(theta.cos(), theta.sin())); + } + } + + let arr = Array::from_vec(complex_arr); + let angles = arr.angle(false); + + // All angles should be in range (-π, π] + for &angle in angles.iter() { + assert!(angle > -PI && angle <= PI, "Angle {} is outside range (-π, π]", angle); + } + } +} diff --git a/src/numeric/mod.rs b/src/numeric/mod.rs index 8b63747f6..6e305e7cc 100644 --- a/src/numeric/mod.rs +++ b/src/numeric/mod.rs @@ -3,4 +3,4 @@ mod impl_numeric; mod impl_float_maths; #[cfg(feature = "std")] -pub use self::impl_float_maths::{angle, angle_preserve, angle_scalar, HasAngle, HasAngle64}; +pub use self::impl_float_maths::{angle, angle_scalar, HasAngle}; From 213712e7513d62eaff1ea85752a5bc97362ff195 Mon Sep 17 00:00:00 2001 From: Jack Geraghty Date: Sat, 8 Nov 2025 17:08:49 +0000 Subject: [PATCH 3/3] Removed "*_preserve" traces and duplicate function. Removed deg arguments - Tidied up the comments/documentation to remove all traces of the originally version of the PR which included float promotions. - Removed the duplicate angle function - Removed the deg parameter in the various functions. This leaves the conversion up to the user and can easily be achieved with the Float::to_degrees function - Tidied some trait bounds which did not need the FloatConst trait - Ran ``cargo fmt`` --- src/numeric/impl_float_maths.rs | 383 +++++++++++++------------------- 1 file changed, 151 insertions(+), 232 deletions(-) diff --git a/src/numeric/impl_float_maths.rs b/src/numeric/impl_float_maths.rs index be764052e..5ddab4707 100644 --- a/src/numeric/impl_float_maths.rs +++ b/src/numeric/impl_float_maths.rs @@ -1,40 +1,39 @@ // Element-wise methods for ndarray -#[cfg(feature = "std")] -use num_traits::{Float, FloatConst, NumCast}; #[cfg(feature = "std")] use num_complex::Complex; +#[cfg(feature = "std")] +use num_traits::Float; use crate::imp_prelude::*; -/// Optional: precision-preserving variant (returns `F`), if you want -/// an API that keeps `f32` outputs for `f32` inputs. -/// -/// - Works for `f32`/`f64` and `Complex`/`Complex`. +/// Trait for types that can generalize the phase angle (argument) +/// calculation for both real floats and complex numbers. #[cfg(feature = "std")] -pub trait HasAngle { - /// Return the phase angle (argument) in the same precision as the input type. +pub trait HasAngle +{ + /// Return the phase angle (argument) of the value fn to_angle(&self) -> F; } #[cfg(feature = "std")] impl HasAngle for F -where - F: Float + FloatConst, +where F: Float { #[inline] - fn to_angle(&self) -> F { + fn to_angle(&self) -> F + { F::zero().atan2(*self) } } #[cfg(feature = "std")] impl HasAngle for Complex -where - F: Float + FloatConst, +where F: Float { #[inline] - fn to_angle(&self) -> F { + fn to_angle(&self) -> F + { self.im.atan2(self.re) } } @@ -207,17 +206,13 @@ where #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl ArrayRef -where - D: Dimension, +where D: Dimension { /// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of complex values in the array. /// - /// This function always returns `f64` values, regardless of input precision. - /// The angles are returned in the range (-π, π]. - /// - /// # Arguments - /// - /// * `deg` - If `true`, convert radians to degrees; if `false`, return radians. + /// This function always returns the same float type as was provided to it. Leaving the exact precision left to the user. + /// The angles are returned in ``radians`` and in the range ``(-π, π]``. + /// To get the angles in degrees, use the `to_degrees()` method on the resulting array. /// /// # Examples /// @@ -228,8 +223,8 @@ where /// /// // Real numbers /// let real_arr = array![1.0f64, -1.0, 0.0]; - /// let angles_rad = real_arr.angle(false); - /// let angles_deg = real_arr.angle(true); + /// let angles_rad = real_arr.angle(); + /// let angles_deg = real_arr.angle().to_degrees(); /// assert!((angles_rad[0] - 0.0).abs() < 1e-10); /// assert!((angles_rad[1] - PI).abs() < 1e-10); /// assert!((angles_deg[1] - 180.0).abs() < 1e-10); @@ -246,65 +241,10 @@ where /// assert!((angles[2] - PI/4.0).abs() < 1e-10); /// ``` #[must_use = "method returns a new array and does not mutate the original value"] - pub fn angle(&self, deg: bool) -> Array - where - A: HasAngle, - { - let mut result = self.map(|x| x.to_angle()); - if deg { - result.mapv_inplace(|a| a * F::from(180.0).unwrap() / F::PI()); - } - result - } -} - -/// # Precision-preserving angle calculation methods -/// -/// Methods for calculating phase angles that preserve input precision. -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -impl ArrayRef -where - D: Dimension, -{ - /// Return the [phase angle (argument)](https://en.wikipedia.org/wiki/Argument_(complex_analysis)) of values, preserving input precision. - /// - /// This method preserves the precision of the input: - /// - `f32` and `Complex` inputs produce `f32` outputs - /// - `f64` and `Complex` inputs produce `f64` outputs - /// - /// # Arguments - /// - /// * `deg` - If `true`, convert radians to degrees; if `false`, return radians. - /// - /// # Examples - /// - /// ``` - /// use ndarray::array; - /// use num_complex::Complex; - /// - /// // f32 precision preserved for complex numbers - /// let complex_f32 = array![Complex::new(1.0f32, 1.0f32)]; - /// let angles_f32 = complex_f32.angle_preserve(false); - /// // angles_f32 has type Array - /// - /// // f64 precision preserved for complex numbers - /// let complex_f64 = array![Complex::new(1.0f64, 1.0f64)]; - /// let angles_f64 = complex_f64.angle_preserve(false); - /// // angles_f64 has type Array - /// ``` - #[must_use = "method returns a new array and does not mutate the original value"] - pub fn angle_preserve(&self, deg: bool) -> Array - where - A: HasAngle, - F: Float + FloatConst, + pub fn angle(&self) -> Array + where A: HasAngle + Clone { - let mut result = self.map(|x| x.to_angle()); - if deg { - let factor = F::from(180.0).unwrap() / F::PI(); - result.mapv_inplace(|a| a * factor); - } - result + self.mapv(|f| A::to_angle(&f)) } } @@ -340,7 +280,6 @@ where /// # Arguments /// /// * `z` - A real or complex value (f32/f64, `Complex`/`Complex`). -/// * `deg` - If `true`, convert radians to degrees. /// /// # Returns /// @@ -352,37 +291,26 @@ where /// use num_complex::Complex; /// use std::f64::consts::PI; /// -/// assert!((ndarray::angle_scalar(Complex::new(1.0f64, 1.0), false) - PI/4.0).abs() < 1e-10); -/// assert!((ndarray::angle_scalar(1.0f32, true) - 0.0).abs() < 1e-10); -/// assert!((ndarray::angle_scalar(-1.0f32, true) - 180.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(Complex::new(1.0f64, 1.0)) - PI/4.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(1.0f32) - 0.0).abs() < 1e-10); +/// assert!((ndarray::angle_scalar(-1.0f32) - 180.0).abs() < 1e-10); /// ``` #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] -pub fn angle_scalar>(z: T, deg: bool) -> F +pub fn angle_scalar>(z: T) -> F { - let mut a = z.to_angle(); - if deg { - - a = a * ::from(180.0).expect("180.0 is a valid f32 and f64 -- this should not fail") / F::PI(); - } - a + z.to_angle() } -/// Precision-preserving angle calculation function. -/// -/// Calculate the phase angle of complex values while preserving input precision. -/// Unlike [`angle`], this function returns the same precision as the input: -/// - `f32` and `Complex` inputs produce `f32` outputs -/// - `f64` and `Complex` inputs produce `f64` outputs +/// Calculate the phase angle of complex values. /// /// # Arguments /// /// * `z` - Array of real or complex values. -/// * `deg` - If `true`, convert radians to degrees. /// /// # Returns /// -/// An `Array` with the same shape as `z` and precision matching the input. +/// An `Array` with the same shape as `z`. /// /// # Examples /// @@ -390,229 +318,220 @@ pub fn angle_scalar>(z: T, deg: bool) -> F /// use ndarray::array; /// use num_complex::Complex; /// -/// // f32 precision preserved for complex numbers +/// // f32 precision for complex numbers /// let z32 = array![Complex::new(0.0f32, 1.0)]; -/// let out32 = ndarray::angle(&z32, false); +/// let out32 = ndarray::angle(&z32); /// // out32 has type Array /// -/// // f64 precision preserved for complex numbers +/// // f64 precision for complex numbers /// let z64 = array![Complex::new(0.0f64, -1.0)]; -/// let out64 = ndarray::angle(&z64, false); +/// let out64 = ndarray::angle(&z64); /// // out64 has type Array /// ``` #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] -pub fn angle(z: &ArrayBase, deg: bool) -> Array +pub fn angle(z: &ArrayBase) -> Array where A: HasAngle, - F: Float + FloatConst, + F: Float, S: Data, D: Dimension, { - let mut result = z.map(|x| x.to_angle()); - if deg { - let factor = F::from(180.0).unwrap() / F::PI(); - result.mapv_inplace(|a| a * factor); - } - result + z.map(HasAngle::to_angle) } #[cfg(all(test, feature = "std"))] -mod angle_tests { +mod angle_tests +{ use super::*; use crate::Array; use num_complex::Complex; use std::f64::consts::PI; + /// Helper macro for floating-point comparison + macro_rules! assert_approx_eq { + ($a:expr, $b:expr, $tol:expr $(, $msg:expr)?) => {{ + let (a, b) = ($a, $b); + assert!( + (a - b).abs() < $tol, + concat!( + "assertion failed: |left - right| >= tol\n", + " left: {left:?}\n right: {right:?}\n tol: {tol:?}\n", + $($msg,)? + ), + left = a, + right = b, + tol = $tol + ); + }}; + } + #[test] - fn test_real_numbers_radians() { + fn test_real_numbers_radians() + { let arr = Array::from_vec(vec![1.0f64, -1.0, 0.0]); - let angles = arr.angle(false); + let angles = arr.angle(); - assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1.0) should be 0"); - assert!((angles[1] - PI).abs() < 1e-10, "angle(-1.0) should be π"); - assert!(angles[2].abs() < 1e-10, "angle(0.0) should be 0"); + assert_approx_eq!(angles[0], 0.0, 1e-10, "angle(1.0) should be 0"); + assert_approx_eq!(angles[1], PI, 1e-10, "angle(-1.0) should be π"); + assert_approx_eq!(angles[2], 0.0, 1e-10, "angle(0.0) should be 0"); } #[test] - fn test_real_numbers_degrees() { + fn test_real_numbers_degrees() + { let arr = Array::from_vec(vec![1.0f64, -1.0, 0.0]); - let angles = arr.angle(true); + let angles_deg = arr.angle().to_degrees(); - assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1.0) should be 0°"); - assert!((angles[1] - 180.0).abs() < 1e-10, "angle(-1.0) should be 180°"); - assert!(angles[2].abs() < 1e-10, "angle(0.0) should be 0°"); + assert_approx_eq!(angles_deg[0], 0.0, 1e-10, "angle(1.0) should be 0°"); + assert_approx_eq!(angles_deg[1], 180.0, 1e-10, "angle(-1.0) should be 180°"); + assert_approx_eq!(angles_deg[2], 0.0, 1e-10, "angle(0.0) should be 0°"); } #[test] - fn test_complex_numbers_radians() { + fn test_complex_numbers_radians() + { let arr = Array::from_vec(vec![ - Complex::new(1.0f64, 0.0), // 0 - Complex::new(0.0, 1.0), // π/2 - Complex::new(-1.0, 0.0), // π - Complex::new(0.0, -1.0), // -π/2 - Complex::new(1.0, 1.0), // π/4 - Complex::new(-1.0, -1.0), // -3π/4 + Complex::new(1.0, 0.0), // 0 + Complex::new(0.0, 1.0), // π/2 + Complex::new(-1.0, 0.0), // π + Complex::new(0.0, -1.0), // -π/2 + Complex::new(1.0, 1.0), // π/4 + Complex::new(-1.0, -1.0), // -3π/4 ]); - let angles = arr.angle(false); - - assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1+0i) should be 0"); - assert!((angles[1] - PI/2.0).abs() < 1e-10, "angle(0+1i) should be π/2"); - assert!((angles[2] - PI).abs() < 1e-10, "angle(-1+0i) should be π"); - assert!((angles[3] - (-PI/2.0)).abs() < 1e-10, "angle(0-1i) should be -π/2"); - assert!((angles[4] - PI/4.0).abs() < 1e-10, "angle(1+1i) should be π/4"); - assert!((angles[5] - (-3.0*PI/4.0)).abs() < 1e-10, "angle(-1-1i) should be -3π/4"); + let a = arr.angle(); + + assert_approx_eq!(a[0], 0.0, 1e-10); + assert_approx_eq!(a[1], PI / 2.0, 1e-10); + assert_approx_eq!(a[2], PI, 1e-10); + assert_approx_eq!(a[3], -PI / 2.0, 1e-10); + assert_approx_eq!(a[4], PI / 4.0, 1e-10); + assert_approx_eq!(a[5], -3.0 * PI / 4.0, 1e-10); } #[test] - fn test_complex_numbers_degrees() { + fn test_complex_numbers_degrees() + { let arr = Array::from_vec(vec![ - Complex::new(1.0f64, 0.0), + Complex::new(1.0, 0.0), Complex::new(0.0, 1.0), Complex::new(-1.0, 0.0), Complex::new(1.0, 1.0), ]); - let angles = arr.angle(true); + let a = arr.angle().to_degrees(); - assert!((angles[0] - 0.0).abs() < 1e-10, "angle(1+0i) should be 0°"); - assert!((angles[1] - 90.0).abs() < 1e-10, "angle(0+1i) should be 90°"); - assert!((angles[2] - 180.0).abs() < 1e-10, "angle(-1+0i) should be 180°"); - assert!((angles[3] - 45.0).abs() < 1e-10, "angle(1+1i) should be 45°"); + assert_approx_eq!(a[0], 0.0, 1e-10); + assert_approx_eq!(a[1], 90.0, 1e-10); + assert_approx_eq!(a[2], 180.0, 1e-10); + assert_approx_eq!(a[3], 45.0, 1e-10); } #[test] - fn test_signed_zeros() { - let arr = Array::from_vec(vec![ - Complex::new(0.0f64, 0.0), // +0 + 0i → +0 - Complex::new(-0.0, 0.0), // -0 + 0i → +π - Complex::new(0.0, -0.0), // +0 - 0i → -0 - Complex::new(-0.0, -0.0), // -0 - 0i → -π - ]); - let angles = arr.angle(false); - - assert!(angles[0] >= 0.0 && angles[0].abs() < 1e-10, "+0+0i should give +0"); - assert!((angles[1] - PI).abs() < 1e-10, "-0+0i should give +π"); - assert!(angles[2] <= 0.0 && angles[2].abs() < 1e-10, "+0-0i should give -0"); - assert!((angles[3] - (-PI)).abs() < 1e-10, "-0-0i should give -π"); - } - - #[test] - fn test_angle_preserve_f32() { - let arr = Array::from_vec(vec![ - Complex::new(1.0f32, 1.0), - Complex::new(-1.0, 0.0), - ]); - let angles = arr.angle_preserve(false); - - assert!((angles[0] - std::f32::consts::FRAC_PI_4).abs() < 1e-6); - assert!((angles[1] - std::f32::consts::PI).abs() < 1e-6); - } - - #[test] - fn test_angle_preserve_f64() { + fn test_signed_zeros() + { let arr = Array::from_vec(vec![ - Complex::new(1.0f64, 1.0), - Complex::new(-1.0, 0.0), + Complex::new(0.0, 0.0), + Complex::new(-0.0, 0.0), + Complex::new(0.0, -0.0), + Complex::new(-0.0, -0.0), ]); - let angles = arr.angle_preserve(false); + let a = arr.angle(); - assert!((angles[0] - PI/4.0).abs() < 1e-10); - assert!((angles[1] - PI).abs() < 1e-10); + assert!(a[0] >= 0.0 && a[0].abs() < 1e-10); + assert_approx_eq!(a[1], PI, 1e-10); + assert!(a[2] <= 0.0 && a[2].abs() < 1e-10); + assert_approx_eq!(a[3], -PI, 1e-10); } #[test] - fn test_angle_scalar_f64() { - assert!((angle_scalar(Complex::new(1.0f64, 1.0), false) - PI/4.0).abs() < 1e-10); - assert!((angle_scalar(1.0f64, false) - 0.0).abs() < 1e-10); - assert!((angle_scalar(-1.0f64, false) - PI).abs() < 1e-10); - assert!((angle_scalar(-1.0f64, true) - 180.0).abs() < 1e-10); + fn test_angle_scalar_f64() + { + assert_approx_eq!(angle_scalar(Complex::new(1.0, 1.0)), PI / 4.0, 1e-10); + assert_approx_eq!(angle_scalar(1.0f64), 0.0, 1e-10); + assert_approx_eq!(angle_scalar(-1.0f64), PI, 1e-10); } #[test] - fn test_angle_scalar_f32() { - assert!((angle_scalar(Complex::new(1.0f32, 1.0), false) - std::f32::consts::FRAC_PI_4).abs() < 1e-6); - assert!((angle_scalar(1.0f32, true) - 0.0).abs() < 1e-6); - assert!((angle_scalar(-1.0f32, true) - 180.0).abs() < 1e-6); + fn test_angle_scalar_f32() + { + use std::f32::consts::FRAC_PI_4; + assert_approx_eq!(angle_scalar(Complex::new(1.0f32, 1.0)), FRAC_PI_4, 1e-6); + assert_approx_eq!(angle_scalar(1.0f32), 0.0, 1e-6); + assert_approx_eq!(angle_scalar(-1.0f32), std::f32::consts::PI, 1e-6); } #[test] - fn test_angle_function() { + fn test_angle_function() + { let arr = Array::from_vec(vec![ - Complex::new(1.0f64, 0.0), + Complex::new(1.0, 0.0), Complex::new(0.0, 1.0), Complex::new(-1.0, 1.0), ]); - let angles = angle(&arr, false); + let a = angle(&arr); - assert!((angles[0] - 0.0).abs() < 1e-10); - assert!((angles[1] - PI/2.0).abs() < 1e-10); - assert!((angles[2] - 3.0*PI/4.0).abs() < 1e-10); + assert_approx_eq!(a[0], 0.0, 1e-10); + assert_approx_eq!(a[1], PI / 2.0, 1e-10); + assert_approx_eq!(a[2], 3.0 * PI / 4.0, 1e-10); } #[test] - fn test_angle_function_degrees() { + fn test_angle_function_degrees() + { let arr = Array::from_vec(vec![ Complex::new(1.0f32, 1.0), - Complex::new(-1.0, 0.0), + Complex::new(-1.0f32, 0.0), ]); - let angles = angle(&arr, true); + let a = angle(&arr); - assert!((angles[0] - 45.0).abs() < 1e-6); - assert!((angles[1] - 180.0).abs() < 1e-6); + assert_approx_eq!(a[0], 45.0f32.to_radians(), 1e-6); + assert_approx_eq!(a[1], 180.0f32.to_radians(), 1e-6); } #[test] - fn test_edge_cases() { + fn test_edge_cases() + { let arr = Array::from_vec(vec![ Complex::new(f64::INFINITY, 0.0), Complex::new(0.0, f64::INFINITY), Complex::new(f64::NEG_INFINITY, 0.0), Complex::new(0.0, f64::NEG_INFINITY), ]); - let angles = arr.angle(false); + let a = arr.angle(); - assert!((angles[0] - 0.0).abs() < 1e-10, "angle(∞+0i) should be 0"); - assert!((angles[1] - PI/2.0).abs() < 1e-10, "angle(0+∞i) should be π/2"); - assert!((angles[2] - PI).abs() < 1e-10, "angle(-∞+0i) should be π"); - assert!((angles[3] - (-PI/2.0)).abs() < 1e-10, "angle(0-∞i) should be -π/2"); + assert_approx_eq!(a[0], 0.0, 1e-10); + assert_approx_eq!(a[1], PI / 2.0, 1e-10); + assert_approx_eq!(a[2], PI, 1e-10); + assert_approx_eq!(a[3], -PI / 2.0, 1e-10); } #[test] - fn test_mixed_precision() { - // Test that f32 and f64 can be mixed in the same operation + fn test_mixed_precision() + { let arr_f32 = Array::from_vec(vec![1.0f32, -1.0f32]); - let angles_f32 = arr_f32.angle(false); - let arr_f64 = Array::from_vec(vec![1.0f64, -1.0f64]); - let angles_f64 = arr_f64.angle(false); + let a32 = arr_f32.angle(); + let a64 = arr_f64.angle(); - // Results should be equivalent within floating point precision - assert!((angles_f32[0] as f64 - angles_f64[0]).abs() < 1e-6); - assert!((angles_f32[1] as f64 - angles_f64[1]).abs() < 1e-6); + assert_approx_eq!(a32[0] as f64, a64[0], 1e-6); + assert_approx_eq!(a32[1] as f64, a64[1], 1e-6); } #[test] - fn test_range_validation() { - // Generate points on the unit circle and verify angle range + fn test_range_validation() + { let n = 16; - let mut complex_arr = Vec::new(); - - for i in 0..n { - let theta = 2.0 * PI * (i as f64) / (n as f64); - if theta <= PI { - complex_arr.push(Complex::new(theta.cos(), theta.sin())); - } else { - // For angles > π, we expect negative result in range (-π, 0] - complex_arr.push(Complex::new(theta.cos(), theta.sin())); - } - } + let complex_arr: Vec<_> = (0..n) + .map(|i| { + let theta = 2.0 * PI * (i as f64) / (n as f64); + Complex::new(theta.cos(), theta.sin()) + }) + .collect(); - let arr = Array::from_vec(complex_arr); - let angles = arr.angle(false); + let a = Array::from_vec(complex_arr).angle(); - // All angles should be in range (-π, π] - for &angle in angles.iter() { - assert!(angle > -PI && angle <= PI, "Angle {} is outside range (-π, π]", angle); + for &x in &a { + assert!(x > -PI && x <= PI, "Angle {} outside (-π, π]", x); } } }