Skip to main content

polycool/
yuksel.rs

1// Copyright 2025 the Kurbo Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! An implementation of Yuksel's robust version of Newton's algorithm.
5
6fn different_signs(x: f64, y: f64) -> bool {
7    (x < 0.0) != (y < 0.0)
8}
9
10pub(crate) fn find_root<F: Fn(f64) -> f64, DF: Fn(f64) -> f64>(
11    f: F,
12    deriv: DF,
13    mut lower: f64,
14    mut upper: f64,
15    val_lower: f64,
16    val_upper: f64,
17    x_error: f64,
18) -> f64 {
19    if !val_lower.is_finite() || !val_upper.is_finite() {
20        return f64::NAN;
21    }
22    debug_assert!(different_signs(val_lower, val_upper));
23
24    let mut x = lower + (upper - lower) / 2.0;
25    let mut step = (upper - lower) / 2.0;
26
27    if step.abs() <= x_error {
28        return x;
29    }
30
31    while step.abs() > x_error && x.is_finite() {
32        // There's potentially a performance gain to be had by evaluating the
33        // derivative and the polynomial "jointly", so that subexpressions can
34        // be shared. At least for the cubic case, the compiler is smart enough
35        // to do that for us.
36        let deriv_x = deriv(x);
37        let val_x = f(x);
38
39        // By returning early on zero, we make it impossible for `step` to
40        // be NaN. This seems to benchmark a little better than checking for
41        // NaN after the fact.
42        if val_x == 0.0 {
43            return x;
44        }
45        let root_in_first_half = different_signs(val_lower, val_x);
46        if root_in_first_half {
47            upper = x;
48        } else {
49            lower = x;
50        }
51
52        debug_assert!(deriv_x.is_finite());
53        debug_assert!(val_x.is_finite());
54
55        step = -val_x / deriv_x;
56        let mut new_x = x + step;
57
58        if new_x <= lower || new_x >= upper {
59            new_x = lower + (upper - lower) / 2.0;
60
61            if new_x == upper || new_x == lower {
62                // This should be rare, but it happens if they ask for more
63                // accuracy than is reasonable. For example, suppose (because
64                // of large coefficients) the output value jumps from -1.0
65                // to 1.0 between adjacent floats and they ask for an output
66                // error of smaller than 0.5. Then we'll eventually shrink
67                // the search interval to a pair of adjacent floats and hit
68                // this case.
69                return new_x;
70            }
71        }
72        step = new_x - x;
73        x = new_x;
74    }
75    x
76}