Skip to main content

polycool/
quadratic.rs

1// Copyright 2025 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use arrayvec::ArrayVec;
5
6use crate::Quadratic;
7
8#[cfg(feature = "libm")]
9#[allow(unused_imports, reason = "unused if libm and std are both around")]
10use crate::libm_polyfill::FloatFuncs as _;
11
12impl Quadratic {
13    /// This is like [`Quadratic::eval`] but faster.
14    ///
15    /// It would be nice if we could just make `eval` like this, but I couldn't
16    /// figure out how, given the lack of specialization.
17    #[doc(hidden)]
18    pub fn eval_opt(&self, x: f64) -> f64 {
19        let [c0, c1, c2] = self.coeffs;
20        c0 + c1 * x + c2 * x * x
21    }
22
23    /// Returns the roots of this quadratic, in increasing order.
24    ///
25    /// Double-roots are only counted once.
26    pub fn roots(&self) -> ArrayVec<f64, 2> {
27        let &[c, b, a] = self.coeffs();
28        let disc = b * b - 4.0 * a * c;
29        if disc.is_finite() {
30            let mut ret = ArrayVec::new();
31            let mut push = |r: f64| {
32                if r.is_finite() {
33                    ret.push(r)
34                }
35            };
36            if disc > 0.0 {
37                let q = -0.5 * (b + disc.sqrt().copysign(b));
38                let r0 = q / a;
39                let r1 = c / q;
40                push(r0.min(r1));
41                push(r0.max(r1));
42            } else if disc == 0.0 {
43                let root = -0.5 * b / a;
44                if root.is_finite() {
45                    push(root);
46                } else if c == 0.0 {
47                    // This is kurbo's behavior: the intention is that if the
48                    // whole thing is zero, return zero as a single root. I'm
49                    // not sure I love it.
50                    //
51                    // Bear in mind that this branch is not *only* for the
52                    // identically zero case: if a == c == 0.0 and b * b
53                    // underflows then we will end up here. In that case,
54                    // zero is the only root.
55                    push(0.0);
56                }
57            } else {
58                // No roots.
59            }
60            ret
61        } else {
62            // At least one of the coefficients was too large and triggered
63            // overflow.
64            //
65            // The exponent of f64 maxes out at 1023, so scaling down by
66            // 2^{-512} is enough to ensure that squaring doesn't overflow. We
67            // do an extra factor of 2^{-3} for some wiggle room. This can't
68            // completely destroy all the coefficients: because of the overflow,
69            // we know that at least one of them was big.
70            let scale = 2.0f64.powi(-515);
71            // If we're infinite, just give up. (Otherwise, we'd stack overflow
72            // by repeatedly trying to rescale.)
73            if self.is_finite() {
74                (*self * scale).roots()
75            } else {
76                ArrayVec::new()
77            }
78        }
79    }
80
81    /// Returns the two distinct roots of this quadratic, but only if the
82    /// discriminant is positive.
83    pub fn positive_discriminant_roots(&self) -> Option<(f64, f64)> {
84        let &[c, b, a] = self.coeffs();
85        let disc = b * b - 4.0 * a * c;
86        if disc.is_finite() {
87            if disc > 0.0 {
88                let q = -0.5 * (b + disc.sqrt().copysign(b));
89                let r0 = q / a;
90                let r1 = c / q;
91                Some((r0.min(r1), r0.max(r1)))
92            } else {
93                None
94            }
95        } else {
96            self.positive_discriminant_roots_scaled()
97        }
98    }
99
100    #[cold]
101    fn positive_discriminant_roots_scaled(&self) -> Option<(f64, f64)> {
102        if self.is_finite() {
103            let scale = 2.0f64.powi(-515);
104            (*self * scale).positive_discriminant_roots()
105        } else {
106            None
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    #[test]
114    fn root_evaluation() {
115        arbtest::arbtest(|u| {
116            let q = crate::arbitrary::quadratic(u)?;
117            // Arbitrary quadratics can have coefficients with wild magnitudes,
118            // so we need to adjust our error expectations accordingly.
119            let magnitude = q.max_abs_coefficient().max(1.0);
120
121            for r in q.roots() {
122                let y = q.eval(r);
123                // To evaluate the polynomial, we need to square r, so our error
124                // should be relative to the magnitude of r squared.
125                let r_magnitude = r.abs().max(1.0);
126                let threshold = r_magnitude * 1e-14 * r_magnitude * magnitude;
127                if y.is_finite() && threshold.is_finite() {
128                    assert!(y.abs() <= threshold);
129                }
130            }
131            Ok(())
132        })
133        .budget_ms(5_000);
134    }
135}