Skip to main content

strided_basic/
complex_div.rs

1//! Scale-robust complex division.
2//!
3//! `num_complex`'s `Div` uses the textbook formula `(a·conj(b)) / (re(b)² +
4//! im(b)²)`, whose denominator overflows to `inf` for `|b| ≳ 2^512` and
5//! underflows to `0` for `|b| ≲ 2^-511`, even when the quotient is
6//! representable. These helpers are a direct port of Julia Base
7//! `base/complex.jl` (`/`, `cdiv`, `robust_cdiv1/2`, `scaling_cdiv`,
8//! `scaleargs_cdiv`), which is Baudin–Smith with power-of-two scaling:
9//!
10//! * the normal-range case keeps the ratio form `(a + b·r)·t`;
11//! * extreme numerator or denominator magnitudes are rescaled by powers of two
12//!   and unscaled afterwards, so the result stays representable;
13//! * `Complex<f32>` widens to `Complex<f64>` and narrows, as Julia does for
14//!   `Complex{Float32}`.
15//!
16//! `inv(z) == 1/z` and `a/b == a·inv(b)`, so only the division is needed here.
17
18use num_complex::{Complex32, Complex64};
19
20/// Divide two `Complex<f64>` values without the `|z|²` overflow/underflow of
21/// the textbook formula.
22#[inline]
23pub fn robust_complex_divide_f64(a: Complex64, b: Complex64) -> Complex64 {
24    let (ar, ai) = (a.re, a.im);
25    let (br, bi) = (b.re, b.im);
26    if br.is_infinite() || bi.is_infinite() {
27        if ar.is_finite() && ai.is_finite() {
28            return Complex64::new(
29                0.0 * ar.signum() * br.signum(),
30                -0.0 * ai.signum() * bi.signum(),
31            );
32        }
33        return Complex64::new(f64::NAN, f64::NAN);
34    }
35    // Julia deliberately uses a select instead of `max` here so that a NaN
36    // component does not change the scaling branch (and stays branch-free).
37    let abs_ar = ar.abs();
38    let abs_ai = ai.abs();
39    let ab = if abs_ar >= abs_ai { abs_ar } else { abs_ai };
40    let abs_br = br.abs();
41    let abs_bi = bi.abs();
42    let cd = if abs_br >= abs_bi { abs_br } else { abs_bi };
43    if ab >= 0.5 * f64::MAX
44        || ab <= f64::MIN_POSITIVE * 2.0 / f64::EPSILON
45        || cd >= 0.5 * f64::MAX
46        || cd <= f64::MIN_POSITIVE * 2.0 / f64::EPSILON
47    {
48        scaling_cdiv_f64(ar, ai, br, bi, ab, cd)
49    } else {
50        cdiv_f64(ar, ai, br, bi)
51    }
52}
53
54/// Divide two `Complex<f32>` values by widening to `Complex<f64>` (Julia's
55/// `Complex{Float32}` route), so the `f32` squares cannot overflow.
56#[inline]
57pub fn robust_complex_divide_f32(a: Complex32, b: Complex32) -> Complex32 {
58    let (ar, ai) = (f64::from(a.re), f64::from(a.im));
59    let (br, bi) = (f64::from(b.re), f64::from(b.im));
60    if br.is_infinite() || bi.is_infinite() {
61        if ar.is_finite() && ai.is_finite() {
62            return Complex32::new(
63                (0.0 * ar.signum() * br.signum()) as f32,
64                (-0.0 * ai.signum() * bi.signum()) as f32,
65            );
66        }
67        return Complex32::new(f32::NAN, f32::NAN);
68    }
69    let mag = 1.0 / br.mul_add(br, bi * bi);
70    let re = ar.mul_add(br, ai * bi);
71    let im = ai.mul_add(br, -ar * bi);
72    Complex32::new((re * mag) as f32, (im * mag) as f32)
73}
74
75#[inline]
76fn cdiv_f64(a: f64, b: f64, c: f64, d: f64) -> Complex64 {
77    if d.abs() <= c.abs() {
78        robust_cdiv1(a, b, c, d)
79    } else {
80        let swapped = robust_cdiv1(b, a, d, c);
81        Complex64::new(swapped.re, -swapped.im)
82    }
83}
84
85#[inline]
86fn scaling_cdiv_f64(a: f64, b: f64, c: f64, d: f64, ab: f64, cd: f64) -> Complex64 {
87    let (a, b, c, d, s) = scale_cdiv_args(a, b, c, d, ab, cd);
88    let quotient = cdiv_f64(a, b, c, d);
89    Complex64::new(quotient.re * s, quotient.im * s)
90}
91
92fn scale_cdiv_args(a: f64, b: f64, c: f64, d: f64, ab: f64, cd: f64) -> (f64, f64, f64, f64, f64) {
93    let half_ov = 0.5 * f64::MAX;
94    let two_un_eps = f64::MIN_POSITIVE * 2.0 / f64::EPSILON;
95    let big_scale = 2.0 / (f64::EPSILON * f64::EPSILON);
96    let mut s = 1.0;
97    let (mut a, mut b, mut c, mut d) = (a, b, c, d);
98    if ab >= half_ov {
99        a *= 0.5;
100        b *= 0.5;
101        s *= 2.0;
102    } else if ab <= two_un_eps {
103        a *= big_scale;
104        b *= big_scale;
105        s /= big_scale;
106    }
107    if cd >= half_ov {
108        c *= 0.5;
109        d *= 0.5;
110        s *= 0.5;
111    } else if cd <= two_un_eps {
112        c *= big_scale;
113        d *= big_scale;
114        s *= big_scale;
115    }
116    (a, b, c, d, s)
117}
118
119#[inline]
120fn robust_cdiv1(a: f64, b: f64, c: f64, d: f64) -> Complex64 {
121    let r = d / c;
122    let t = 1.0 / (c + d * r);
123    Complex64::new(
124        robust_cdiv2(a, b, c, d, r, t),
125        robust_cdiv2(b, -a, c, d, r, t),
126    )
127}
128
129#[inline]
130fn robust_cdiv2(a: f64, b: f64, c: f64, d: f64, r: f64, t: f64) -> f64 {
131    if r != 0.0 {
132        let br = b * r;
133        if br != 0.0 {
134            (a + br) * t
135        } else {
136            a * t + (b * t) * r
137        }
138    } else {
139        (a + d * (b / c)) * t
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    /// Highest and lowest representable `f64` powers used by the issue's cases.
148    fn c64(re: f64, im: f64) -> Complex64 {
149        Complex64::new(re, im)
150    }
151
152    #[test]
153    fn normal_range_matches_the_textbook_formula() {
154        let a = c64(3.0, 4.0);
155        let b = c64(1.0, -2.0);
156        let expected = a / b;
157        let actual = robust_complex_divide_f64(a, b);
158        assert!((actual.re - expected.re).abs() <= 1e-15 * expected.re.abs().max(1.0));
159        assert!((actual.im - expected.im).abs() <= 1e-15 * expected.im.abs().max(1.0));
160    }
161
162    #[test]
163    fn huge_denominator_stays_representable() {
164        // 1 / (2^600 + 2^600 i) == 2^-601 (1 - i)
165        let scale = 2f64.powi(600);
166        let b = c64(scale, scale);
167        let actual = robust_complex_divide_f64(c64(1.0, 0.0), b);
168        let expected = 2f64.powi(-601);
169        assert!(
170            (actual.re - expected).abs() <= expected * 1e-15,
171            "{actual:?}"
172        );
173        assert!(
174            (actual.im + expected).abs() <= expected * 1e-15,
175            "{actual:?}"
176        );
177    }
178
179    #[test]
180    fn tiny_denominator_stays_representable() {
181        // 1 / (2^-600 + 2^-600 i) == 2^599 (1 - i)
182        let scale = 2f64.powi(-600);
183        let b = c64(scale, scale);
184        let actual = robust_complex_divide_f64(c64(1.0, 0.0), b);
185        let expected = 2f64.powi(599);
186        assert!(
187            (actual.re - expected).abs() <= expected * 1e-15,
188            "{actual:?}"
189        );
190        assert!(
191            (actual.im + expected).abs() <= expected * 1e-15,
192            "{actual:?}"
193        );
194    }
195
196    #[test]
197    fn infinite_components_produce_signed_zeros() {
198        let actual = robust_complex_divide_f64(c64(1.0, 2.0), c64(f64::INFINITY, 1.0));
199        assert_eq!(actual.re, 0.0);
200        assert!(
201            actual.im.is_sign_negative() && actual.im == 0.0,
202            "{actual:?}"
203        );
204    }
205
206    #[test]
207    fn f32_widens_so_the_squares_cannot_overflow() {
208        let scale = 2f32.powi(60);
209        let actual =
210            robust_complex_divide_f32(Complex32::new(1.0, 0.0), Complex32::new(scale, scale));
211        let expected = 2f32.powi(-61);
212        assert!(
213            (actual.re - expected).abs() <= expected * 1e-6,
214            "{actual:?}"
215        );
216        assert!(
217            (actual.im + expected).abs() <= expected * 1e-6,
218            "{actual:?}"
219        );
220    }
221}