From 9c9cb95a7f0d9e90bc69de9551e5d5d965653d49 Mon Sep 17 00:00:00 2001 From: Mark Nefedov Date: Fri, 14 Aug 2026 23:41:01 +0300 Subject: [PATCH] Fix NaN lanes in the default SimdComplexField::simd_to_exp The default implementation tested whether the modulus was zero on all lanes at once, so mixed zero/nonzero inputs took the division path and computed 0 / 0 on the zero lanes. The resulting NaNs propagated through simd_signum. The zero test is now lane-wise: the division uses a safe modulus and the identity result is selected on the zero lanes. Reachable through num_complex::Complex over any SIMD type that does not override the default (the Wide* wrappers among them); includes a regression test for the mixed-lane and all-zero cases. --- src/simd/simd_complex.rs | 19 +++++++++------ tests/simd_complex_to_exp.rs | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 tests/simd_complex_to_exp.rs diff --git a/src/simd/simd_complex.rs b/src/simd/simd_complex.rs index f15b398..8e0a4dd 100644 --- a/src/simd/simd_complex.rs +++ b/src/simd/simd_complex.rs @@ -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. /// @@ -67,11 +67,16 @@ SubsetOf 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()` diff --git a/tests/simd_complex_to_exp.rs b/tests/simd_complex_to_exp.rs new file mode 100644 index 0000000..29a7301 --- /dev/null +++ b/tests/simd_complex_to_exp.rs @@ -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 = 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]); +}