poly_cool/
cubic.rs

1use arrayvec::ArrayVec;
2
3use crate::{InputError, Quadratic, TerminationCondition, ValueError, different_signs};
4
5#[derive(Debug, Copy, Clone)]
6pub struct Cubic {
7    pub c0: f64,
8    pub c1: f64,
9    pub c2: f64,
10    pub c3: f64,
11}
12
13impl std::ops::Div<f64> for Cubic {
14    type Output = Cubic;
15
16    fn div(self, rhs: f64) -> Cubic {
17        Cubic {
18            c0: self.c0 / rhs,
19            c1: self.c1 / rhs,
20            c2: self.c2 / rhs,
21            c3: self.c3 / rhs,
22        }
23    }
24}
25
26impl std::ops::Mul<f64> for Cubic {
27    type Output = Cubic;
28
29    fn mul(self, rhs: f64) -> Cubic {
30        Cubic {
31            c0: self.c0 * rhs,
32            c1: self.c1 * rhs,
33            c2: self.c2 * rhs,
34            c3: self.c3 * rhs,
35        }
36    }
37}
38
39impl Cubic {
40    pub fn eval(&self, x: f64) -> f64 {
41        let xx = x * x;
42        let xxx = xx * x;
43        self.c0 + self.c1 * x + self.c2 * xx + self.c3 * xxx
44    }
45
46    pub fn deriv(&self) -> Quadratic {
47        Quadratic {
48            c0: self.c1,
49            c1: 2.0 * self.c2,
50            c2: 3.0 * self.c3,
51        }
52    }
53
54    pub fn max_coeff(&self) -> f64 {
55        self.c0
56            .abs()
57            .max(self.c1.abs())
58            .max(self.c2.abs())
59            .max(self.c3.abs())
60    }
61
62    fn deflate(&self, root: f64) -> Quadratic {
63        let a = self.c3;
64        let b = self.c2 + root * a;
65        let c = self.c1 + root * b;
66        Quadratic {
67            c2: a,
68            c1: b,
69            c0: c,
70        }
71    }
72
73    /// Computes the critical points of this cubic, as long
74    /// as the discriminant of the derivative is positive.
75    /// The return values are in increasing order.
76    ///
77    /// Some corner cases worth noting:
78    ///   - If the discriminant is zero, returns nothing. That is,
79    ///     we don't find double-roots of the derivative.
80    ///   - If the derivative is linear or close to it, we might
81    ///     return +/- infinity as one of the roots.
82    ///   - Unless some input is NaN, we don't return NaN.
83    fn critical_points(&self) -> Option<(f64, f64)> {
84        let a = 3.0 * self.c3;
85        let b_2 = self.c2;
86        let c = self.c1;
87        let disc_4 = b_2 * b_2 - a * c;
88
89        if !disc_4.is_finite() {
90            return self.rescaled_critical_points();
91        }
92
93        if disc_4 > 0.0 {
94            let q = -(b_2 + disc_4.sqrt().copysign(b_2));
95            let r0 = q / a;
96            let r1 = c / q;
97            Some((r0.min(r1), r0.max(r1)))
98        } else {
99            None
100        }
101    }
102
103    #[cold]
104    fn rescaled_critical_points(&self) -> Option<(f64, f64)> {
105        let scale = 2.0f64.powi(-515);
106        (*self * scale).critical_points()
107    }
108
109    fn one_root<Term: TerminationCondition>(
110        &self,
111        mut lower: f64,
112        mut upper: f64,
113        term: Term,
114    ) -> f64 {
115        let val_lower = self.eval(lower);
116        let val_upper = self.eval(upper);
117        if !val_lower.is_finite() || !val_upper.is_finite() || !self.deriv().is_finite() {
118            return f64::NAN;
119        }
120        debug_assert!(different_signs(val_lower, val_upper));
121
122        let mut x = lower + (upper - lower) / 2.0;
123        let mut val_x = self.eval(x);
124        let mut step = (upper - lower) / 2.0;
125
126        while x.is_finite() && !term.stop(step, val_x) {
127            let root_in_first_half = different_signs(val_lower, val_x);
128            if root_in_first_half {
129                upper = x;
130            } else {
131                lower = x;
132            }
133
134            let deriv_x = self.deriv().eval(x);
135            debug_assert!(deriv_x.is_finite());
136            debug_assert!(val_x.is_finite());
137
138            step = -val_x / deriv_x;
139            let mut new_x = x + step;
140
141            if new_x <= lower || new_x >= upper {
142                new_x = lower + (upper - lower) / 2.0;
143
144                if new_x == upper || new_x == lower {
145                    // This should be rare, but it happens if they ask for more
146                    // accuracy than is reasonable. For example, suppse (because
147                    // of large coefficients) the output value jumps from -1.0
148                    // to 1.0 between adjacent floats and they ask for an output
149                    // error of smaller than 0.5. Then we'll eventually shrink
150                    // the search interval to a pair of adjacent floats and hit
151                    // this case.
152                    return new_x;
153                }
154            }
155            step = new_x - x;
156            x = new_x;
157            val_x = self.eval(x);
158        }
159        x
160    }
161
162    fn one_root_precomputed<Term: TerminationCondition>(
163        &self,
164        mut lower: f64,
165        mut upper: f64,
166        val_lower: f64,
167        val_upper: f64,
168        term: Term,
169    ) -> f64 {
170        if !val_lower.is_finite() || !val_upper.is_finite() || !self.deriv().is_finite() {
171            return f64::NAN;
172        }
173        debug_assert!(different_signs(val_lower, val_upper));
174
175        let mut x = lower + (upper - lower) / 2.0;
176        let mut val_x = self.eval(x);
177        let mut step = (upper - lower) / 2.0;
178
179        while x.is_finite() && !term.stop(step, val_x) {
180            let root_in_first_half = different_signs(val_lower, val_x);
181            if root_in_first_half {
182                upper = x;
183            } else {
184                lower = x;
185            }
186
187            let deriv_x = self.deriv().eval(x);
188            debug_assert!(deriv_x.is_finite());
189            debug_assert!(val_x.is_finite());
190
191            step = -val_x / deriv_x;
192            let mut new_x = x + step;
193
194            if new_x <= lower || new_x >= upper {
195                new_x = lower + (upper - lower) / 2.0;
196
197                if new_x == upper || new_x == lower {
198                    // This should be rare, but it happens if they ask for more
199                    // accuracy than is reasonable. For example, suppse (because
200                    // of large coefficients) the output value jumps from -1.0
201                    // to 1.0 between adjacent floats and they ask for an output
202                    // error of smaller than 0.5. Then we'll eventually shrink
203                    // the search interval to a pair of adjacent floats and hit
204                    // this case.
205                    return new_x;
206                }
207            }
208            step = new_x - x;
209            x = new_x;
210            val_x = self.eval(x);
211        }
212        x
213    }
214
215    pub fn root_between_with_output_error(self, lower: f64, upper: f64, y_error: f64) -> f64 {
216        self.one_root(lower, upper, ValueError(y_error))
217    }
218
219    pub fn root_between(self, lower: f64, upper: f64, x_error: f64) -> f64 {
220        self.one_root(lower, upper, InputError(x_error))
221    }
222
223    fn first_root<Term: TerminationCondition>(
224        self,
225        lower: f64,
226        upper: f64,
227        term: Term,
228    ) -> Option<f64> {
229        if let Some((x0, x1)) = self.critical_points() {
230            let possible_endpoints: [f64; 3] = [x0, x1, upper];
231            let mut last = lower;
232            let mut last_val = self.eval(last);
233            for x in possible_endpoints {
234                if x > last && x <= upper {
235                    let val = self.eval(x);
236                    if different_signs(last_val, val) {
237                        return Some(self.one_root_precomputed(last, x, last_val, val, term));
238                    }
239
240                    last = x;
241                    last_val = val;
242                }
243            }
244            None
245        } else {
246            let lower_val = self.eval(lower);
247            let upper_val = self.eval(upper);
248            if different_signs(lower_val, upper_val) {
249                Some(self.one_root_precomputed(lower, upper, lower_val, upper_val, term))
250            } else {
251                None
252            }
253        }
254    }
255
256    pub(crate) fn all_roots_term<Term: TerminationCondition>(
257        self,
258        lower: f64,
259        upper: f64,
260        term: Term,
261    ) -> ArrayVec<f64, 3> {
262        let mut ret = ArrayVec::new();
263        if let Some(r) = self.first_root(lower, upper, term) {
264            ret.push(r);
265            let quad = self.deflate(r);
266            if let Some((x0, x1)) = quad.positive_discriminant_roots() {
267                if lower <= x0 && x0 <= upper {
268                    ret.push(x0);
269                }
270                if lower <= x1 && x1 <= upper {
271                    ret.push(x1);
272                }
273            }
274        }
275        ret
276    }
277
278    pub fn all_roots(self, lower: f64, upper: f64, x_error: f64) -> ArrayVec<f64, 3> {
279        self.all_roots_term(lower, upper, InputError(x_error))
280    }
281
282    pub fn all_roots_with_output_error(
283        self,
284        lower: f64,
285        upper: f64,
286        y_error: f64,
287    ) -> ArrayVec<f64, 3> {
288        self.all_roots_term(lower, upper, ValueError(y_error))
289    }
290
291    /// Computes all roots between `lower` and `upper`, to the desired accuracy.
292    ///
293    /// "Accuracy" is measured with respect to the cubic's value: if this cubic
294    /// is called `f` and we find some `x` with `|f(x)| < accuracy` (and `x` is
295    /// contained between two endpoints where `f` has opposite signs) then we'll
296    /// call `x` a root.
297    ///
298    /// We make no guarantees about multiplicity. In fact, if there's a
299    /// double-root that isn't a triple-root (and therefore has no sign change
300    /// nearby) then there's a good chance we miss it altogether. This is
301    /// fine if you're using this root-finding to optimize a quartic, because
302    /// double-roots of the derivative aren't local extrema.
303    pub fn roots_between_with_output_error(
304        self,
305        lower: f64,
306        upper: f64,
307        y_error: f64,
308    ) -> ArrayVec<f64, 3> {
309        let mut possible_endpoints = ArrayVec::<f64, 3>::new();
310        if let Some((x0, x1)) = self.critical_points() {
311            possible_endpoints.push(x0);
312            possible_endpoints.push(x1);
313        }
314        possible_endpoints.push(upper);
315
316        let mut last = lower;
317        let mut last_val = self.eval(last);
318        let mut ret = ArrayVec::new();
319
320        for x in possible_endpoints {
321            if x > last && x <= upper {
322                let val = self.eval(x);
323                if different_signs(last_val, val) {
324                    ret.push(self.root_between_with_output_error(last, x, y_error));
325                }
326
327                last = x;
328                last_val = val;
329            }
330        }
331        ret
332    }
333
334    #[cold]
335    fn roots_blinn_renormalized(&self) -> ArrayVec<f64, 3> {
336        if !self.max_coeff().is_finite() {
337            ArrayVec::new()
338        } else {
339            (*self / 2.0f64.powi(128)).roots_blinn()
340        }
341    }
342
343    pub fn roots_blinn(&self) -> ArrayVec<f64, 3> {
344        let mut ret = ArrayVec::new();
345        let a = self.c3;
346        let b = self.c2 * (1.0 / 3.0);
347        let c = self.c1 * (1.0 / 3.0);
348        let d = self.c0;
349
350        let delta_1 = a * c - b * b;
351        let delta_2 = a * d - b * c;
352        let delta_3 = b * d - c * c;
353        let disc = 4.0 * delta_1 * delta_3 - delta_2 * delta_2;
354
355        // TODO: what about disc = 0?
356        if disc < 0.0 {
357            dbg!(disc);
358            let (tilde_a, tilde_c, tilde_d) = if b * b * b * d >= a * c * c * c {
359                (a, delta_1, -2.0 * b * delta_1 + a * delta_2)
360            } else {
361                (d, delta_3, -d * delta_2 + 2.0 * c * delta_3)
362            };
363            let t_0 = -tilde_a.copysign(tilde_d) * (-disc).sqrt();
364            let t_1 = -tilde_d + t_0;
365            let p = (t_1 / 2.0).cbrt();
366
367            let q = if t_0 == t_1 { -p } else { -tilde_c / p };
368            let tilde_x = if tilde_c <= 0.0 {
369                p + q
370            } else {
371                -tilde_d / (p * p + q * q + tilde_c)
372            };
373
374            let (x, w) = if b * b * b * d >= a * c * c * c {
375                (tilde_x - b, a)
376            } else {
377                (-d, tilde_x + c)
378            };
379
380            if !x.is_finite() || !w.is_finite() {
381                return self.roots_blinn_renormalized();
382            }
383
384            ret.push(x / w);
385        } else {
386            dbg!(disc);
387            fn one_root(a_or_d: f64, disc: f64, bar_c: f64, bar_d: f64) -> (f64, f64) {
388                let sqrt_c = (-bar_c).sqrt();
389                let theta = (1.0 / 3.0) * (a_or_d * disc.sqrt()).atan2(-bar_d).abs();
390                let (sin_theta, cos_theta) = theta.sin_cos();
391                dbg!(theta, cos_theta);
392                let tilde_x_1 = 2.0 * sqrt_c * cos_theta;
393                let tilde_x_3 = sqrt_c * (-theta.cos() - 3.0f64.sqrt() * sin_theta);
394                (tilde_x_1, tilde_x_3)
395            }
396
397            let bar_c_a = delta_1;
398            let bar_d_a = -2.0 * b * delta_1 + a * delta_2;
399            let (tilde_x_1_a, tilde_x_3_a) = one_root(a, disc, bar_c_a, bar_d_a);
400
401            let bar_c_d = delta_3;
402            let bar_d_d = -d * delta_2 + 2.0 * c * delta_3;
403            let (tilde_x_1_d, tilde_x_3_d) = one_root(d, disc, bar_c_d, bar_d_d);
404
405            let tilde_x_l = if tilde_x_1_a + tilde_x_3_a > 2.0 * b {
406                tilde_x_1_a
407            } else {
408                tilde_x_3_a
409            };
410            let tilde_x_s = if tilde_x_1_d + tilde_x_3_d < 2.0 * c {
411                tilde_x_1_d
412            } else {
413                tilde_x_3_d
414            };
415
416            let (x_l, w_l) = (tilde_x_l - b, a);
417            let (x_s, w_s) = (-d, tilde_x_s + c);
418
419            let e = w_l * w_s;
420            let f = -x_l * w_s - w_l * x_s;
421            let g = x_l * x_s;
422
423            let (x_m, w_m) = (c * f - b * g, c * e - b * f);
424
425            // TODO: check finiteness
426            ret.push(x_l / w_l);
427            ret.push(x_s / w_s);
428            ret.push(x_m / w_m);
429        }
430        ret
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    #[test]
437    fn smoke() {
438        // Here's an example where Blinn's method has a large error. The
439        // small-magnitude root is about -1.0 and the large magnitude root is
440        // apparently of order 1e225, which then causes big errors when trying
441        // to compute the third root. I guess in general we have expect that
442        // the magnitude of the big root affects the error in the middle root.
443        //
444        // Correction: the middle root is actually ok here. It has a pretty
445        // large magnitude (1e17ish), and so it's allowed to not evaluate
446        // super close to zero.
447        // let poly = super::Cubic {
448        //     c0: -3.565233507454652e74,
449        //     c1: -3.5652335074546437e74,
450        //     c2: -1.2298855640101194e-17,
451        //     c3: 9.133009604987547e-243,
452        // };
453
454        let poly = super::Cubic {
455            c0: -3.565233507454652,
456            c1: -3.565233507454643,
457            c2: -1.2298855640101194e-17,
458            c3: 9.133009604987547e-300,
459        };
460
461        // let poly = super::Cubic {
462        //     c0: -5.7227204916679354e194,
463        //     c1: 1.2728341881889333e123,
464        //     c2: 7.093753818594869e-29,
465        //     c3: 9.883719282876428e-181,
466        // };
467        // let poly = super::Cubic {
468        //     c3: 1.0,
469        //     c2: -6.0,
470        //     c1: 11.0,
471        //     c0: -6.0,
472        // };
473        let roots = poly.roots_blinn();
474        dbg!(&roots);
475        for r in roots {
476            dbg!(poly.eval(r));
477        }
478    }
479
480    #[test]
481    fn root_evaluation() {
482        arbtest::arbtest(|u| {
483            let c = crate::arbitrary::cubic(u)?;
484            //dbg!(c);
485            // Arbitrary cubics can have coefficients with wild magnitudes,
486            // so we need to adjust our error expectations accordingly.
487            let magnitude = c.max_coeff().max(1.0);
488            let accuracy = magnitude * 1e-12;
489
490            // We could have a wider range of roots, but then we might need
491            // to lower the accuracy depending on what the actual root is: the
492            // intermediate computations scale like the cube of the root.
493            for r in c.roots_between_with_output_error(-10.0, 10.0, accuracy) {
494                let y = c.eval(r);
495                if y.is_finite() {
496                    assert!(y.abs() <= accuracy);
497                }
498            }
499            for r in c.roots_blinn() {
500                let y = c.eval(r);
501                if y.is_finite() {
502                    dbg!(c, r, y);
503                    assert!(y.abs() <= accuracy);
504                }
505            }
506            for r in c.all_roots_with_output_error(-10.0, 10.0, accuracy) {
507                let y = c.eval(r);
508                if y.is_finite() {
509                    assert!(y.abs() <= accuracy);
510                }
511            }
512            Ok(())
513        })
514        .budget_ms(5_000);
515    }
516
517    #[test]
518    #[ignore]
519    fn root_evaluation_kurbo() {
520        arbtest::arbtest(|u| {
521            let c = crate::arbitrary::cubic(u)?;
522            // Arbitrary cubics can have coefficients with wild magnitudes,
523            // so we need to adjust our error expectations accordingly.
524            let magnitude = c.max_coeff().max(1.0);
525            let accuracy = magnitude * 1e-12;
526
527            // We could have a wider range of roots, but then we might need
528            // to lower the accuracy depending on what the actual root is: the
529            // intermediate computations scale like the cube of the root.
530            for r in kurbo::common::solve_cubic(c.c0, c.c1, c.c2, c.c3) {
531                let y = c.eval(r);
532                if y.is_finite() {
533                    assert!(y.abs() <= accuracy);
534                }
535            }
536            Ok(())
537        })
538        .budget_ms(5_000);
539    }
540}