Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions src/simd/simd_complex.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use num::{NumAssignOps, NumOps, Zero};
use num::{NumAssignOps, NumOps, One, Zero};
use std::any::Any;
use std::f64;
use std::fmt::Debug;
use std::ops::Neg;

use crate::scalar::{ComplexField, Field, SubsetOf, SupersetOf};
use crate::simd::{SimdRealField, SimdValue};
use crate::simd::{SimdBool, SimdPartialOrd, SimdRealField, SimdValue};

/// Lane-wise generalisation of `ComplexField` for SIMD complex fields.
///
Expand Down Expand Up @@ -67,11 +67,16 @@ SubsetOf<Self>
fn simd_to_exp(self) -> (Self::SimdRealField, Self) {
let m = self.clone().simd_modulus();

if !m.is_zero() {
(m.clone(), self.simd_unscale(m))
} else {
(Self::SimdRealField::zero(), Self::one())
}
// Lane-wise zero handling: an all-lanes `is_zero` test would send
// mixed zero/nonzero inputs down the division path and produce NaNs
// (0 / 0) on the zero lanes. Divide by a safe modulus instead, then
// select the identity on the zero lanes.
let is_zero = m.clone().simd_eq(Self::SimdRealField::zero());
let safe_m = is_zero
.clone()
.if_else(Self::SimdRealField::one, || m.clone());
let signum = self.simd_unscale(safe_m);
(m, Self::one().select(is_zero, signum))
}

/// The exponential part of this complex number: `self / self.modulus()`
Expand Down
46 changes: 46 additions & 0 deletions tests/simd_complex_to_exp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Regression test: `simd_to_exp` (and therefore `simd_signum`) must not
//! produce NaN on lanes holding zero when other lanes are nonzero. The old
//! default used a whole-vector `is_zero` test, so mixed inputs divided 0/0.

use num_complex::Complex;
use simba::simd::{SimdComplexField, WideF32x4};

#[test]
fn simd_to_exp_mixed_zero_lanes_produces_no_nan() {
let re = WideF32x4::from_arr([0.0, 3.0, 0.0, -4.0]);
let im = WideF32x4::from_arr([0.0, 4.0, 0.0, 3.0]);
let z = Complex::new(re, im);

let (m, e) = z.simd_to_exp();

let m = m.into_arr();
let e_re = e.re.into_arr();
let e_im = e.im.into_arr();

// No NaNs anywhere.
for v in m.iter().chain(e_re.iter()).chain(e_im.iter()) {
assert!(!v.is_nan(), "NaN leaked out of simd_to_exp: {m:?} {e_re:?} {e_im:?}");
}

// Zero lanes: modulus 0, identity exponential part.
assert_eq!(m[0], 0.0);
assert_eq!((e_re[0], e_im[0]), (1.0, 0.0));
assert_eq!(m[2], 0.0);
assert_eq!((e_re[2], e_im[2]), (1.0, 0.0));

// Nonzero lanes: |3+4i| = 5, direction (0.6, 0.8).
assert!((m[1] - 5.0).abs() < 1e-5);
assert!((e_re[1] - 0.6).abs() < 1e-5);
assert!((e_im[1] - 0.8).abs() < 1e-5);
assert!((m[3] - 5.0).abs() < 1e-5);
assert!((e_re[3] - (-0.8)).abs() < 1e-5);
assert!((e_im[3] - 0.6).abs() < 1e-5);
}

#[test]
fn simd_signum_of_zero_is_one() {
let z: Complex<WideF32x4> = Complex::new(WideF32x4::ZERO, WideF32x4::ZERO);
let s = z.simd_signum();
assert_eq!(s.re.into_arr(), [1.0; 4]);
assert_eq!(s.im.into_arr(), [0.0; 4]);
}