Skip to main content

geop_core_math/solvers/
newton.rs

1//! Newton refinement of a root a subdivision search has isolated — shared
2//! by `geop_core_geometry`'s `NurbCurve::refine_parameter_at_point` and the
3//! intersection searches' `refine_crossing`s — and Newton's method on one
4//! unknown from a seed ([`newton_1d`]), for a point that is a free choice
5//! up to where a function vanishes.
6//!
7//! Subdivision and Newton divide the work. Subdivision is the global
8//! method: it reliably finds and separates every solution, and recognizes
9//! coincidence even for a partial overlap, but converges only one bit per
10//! split — squeezing a parameter down to machine accuracy would take ~50
11//! levels against the ~7 that isolate it. Newton cannot find anything, but
12//! polishes an isolated solution quadratically. So the searches subdivide
13//! to isolate, and a caller about to *use* a parameter — as a split point,
14//! where `NurbCurve::split` cannot absorb a `min_subdivision_size`-wide one
15//! (Boehm insertion amplifies its width without bound) — refines it here.
16
17use std::fmt::Debug;
18
19use crate::{
20    geop_error::{GeopError, GeopResult},
21    scalars::Scalar,
22};
23
24use super::iterate::{End, Iterate, Iterated, Repeat, Step, iterate};
25
26/// Newton iterations. Quadratic convergence makes a handful plenty; this
27/// cannot change which solutions are found, only how tightly an
28/// already-isolated one is pinned down.
29const ITERATIONS: usize = 12;
30
31/// Refine `start` — a box a subdivision search proved holds a root — by
32/// Newton, within `domain`. `step` takes an iterate to the next one,
33/// unsharpened, or `None` where Newton cannot go on (a singular Jacobian at
34/// a tangential crossing or a pole, an evaluation failing).
35///
36/// Every iterate but the last is sharpened, which is legitimate: it is only
37/// a seed for the next step. The final step is left unsharpened, so the
38/// returned widths honestly state how well the root is determined (see
39/// "Sharpen only where the value is a free choice" in `AGENTS.md`). Each
40/// iterate is clamped into `domain` — sharpened first, so the clamp's
41/// comparisons are exact — rather than given up on: a foot point can step
42/// outside the domain on the way in.
43///
44/// A seed repeating one before it ends the iteration early with the same
45/// result ([`super::iterate::Repeat::Cycle`]).
46///
47/// The result is intersected with `start`: both enclose the same root, so
48/// their intersection does too, and is tighter than either. Infallible by
49/// construction: if Newton stops, or ends disjoint from `start` (it has
50/// wandered out of the box the search proved the root lies in), `start` is
51/// returned unchanged. Refinement can only tighten, never fail.
52///
53/// Plain, unverified Newton: it does not itself certify existence or
54/// uniqueness, so callers check that the refined box can still hold what
55/// they are looking for.
56pub fn refine<const N: usize>(
57    start: [Scalar; N],
58    domain: [(Scalar, Scalar); N],
59    step: impl Fn([Scalar; N]) -> Option<[Scalar; N]>,
60) -> [Scalar; N] {
61    let clamp = |x: [Scalar; N]| std::array::from_fn(|i| x[i].clamped(domain[i].0, domain[i].1));
62    let sharp = start.map(|s| s.sharpen());
63    let Ok(Iterated {
64        answer: x,
65        end: End::Exhausted,
66        ..
67    }) = iterate::<_, ()>(
68        sharp,
69        sharp,
70        ITERATIONS,
71        Repeat::Cycle,
72        |next| (clamp(next.map(|s| s.sharpen())), clamp(*next)),
73        // A step over a singular Jacobian can come back as everything: there
74        // is no seed in that, and its middle is no number.
75        |x| {
76            let next = step(*x).filter(|next| next.iter().all(|s| s.is_finite()));
77            Ok(next.map_or(Step::Stuck, Step::Next))
78        },
79    )
80    else {
81        return start;
82    };
83    if (0..N).any(|i| !x[i].could_be_equal(start[i])) {
84        return start;
85    }
86    std::array::from_fn(|i| start[i].intersect(x[i]))
87}
88
89/// A number [`newton_1d`] runs in: plain `f64`, or a scalar.
90pub trait Newton1d: Iterate + Copy + Debug {
91    /// Whether `self`, a function's value, could be zero.
92    fn could_be_zero(self) -> bool;
93    /// The Newton step `value / slope` and its size, or why there is none.
94    fn step(value: Self, slope: Self) -> GeopResult<(Self, f64)>;
95    /// `self - step`, as the seed of the next step: clamped into
96    /// `(lo, hi)`, and sharp — it is a free choice.
97    fn settle(self, step: Self, lo: Self, hi: Self) -> Self;
98}
99
100impl Newton1d for f64 {
101    fn could_be_zero(self) -> bool {
102        self == 0.0
103    }
104    fn step(value: Self, slope: Self) -> GeopResult<(Self, f64)> {
105        if slope == 0.0 {
106            return Err(GeopError::new(format!(
107                "Newton's step: the slope is zero where the value is {value:?}"
108            )));
109        }
110        let step = value / slope;
111        Ok((step, step.abs()))
112    }
113    fn settle(self, step: Self, lo: Self, hi: Self) -> Self {
114        (self - step).clamp(lo, hi)
115    }
116}
117
118/// Sharpened *before* clamping: `clamped`'s comparisons are exact on a
119/// sharp value, so the seed stays in the domain.
120impl Newton1d for Scalar {
121    fn could_be_zero(self) -> bool {
122        self.could_be_equal(Scalar::ZERO)
123    }
124    fn step(value: Self, slope: Self) -> GeopResult<(Self, f64)> {
125        let step = value.div(slope)?;
126        Ok((step, step.abs().upper().to_f64()))
127    }
128    fn settle(self, step: Self, lo: Self, hi: Self) -> Self {
129        self.sub(step).sharpen().clamped(lo, hi)
130    }
131}
132
133/// When [`newton_1d`] has arrived.
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
135pub enum Arrive {
136    /// Where the value could be zero.
137    AtZero,
138    /// Where a step is no shorter than the one before: as close as the
139    /// arithmetic gets.
140    StepsStopShrinking,
141    /// After every step: the seed the last one left.
142    Exhausted,
143}
144
145/// Newton's method on one unknown from `seed`, at most `steps` steps,
146/// within `(lo, hi)`. `at(x)` gives the function's value and slope at `x`.
147/// Every iterate is a sharp seed clamped into `(lo, hi)`: what is returned
148/// is a point as good as any other where the function vanishes, not an
149/// enclosure of the root. `seed` itself is asked as it is.
150///
151/// The last seed, and whether it arrived as `arrive` says (always, for
152/// [`Arrive::Exhausted`]). A slope with no step — zero, or one that could
153/// be — is an error.
154pub fn newton_1d<T: Newton1d, E: From<GeopError>>(
155    seed: T,
156    (lo, hi): (T, T),
157    steps: usize,
158    arrive: Arrive,
159    mut at: impl FnMut(T) -> Result<(T, T), E>,
160) -> Result<(T, bool), E> {
161    let mut last = f64::INFINITY;
162    // Whether a step is done depends on the one before only when it stops
163    // shrinking; otherwise a repeated seed shows where the steps would end.
164    let repeat = match arrive {
165        Arrive::StepsStopShrinking => Repeat::Never,
166        Arrive::AtZero | Arrive::Exhausted => Repeat::Cycle,
167    };
168    let iterated = iterate(
169        seed,
170        seed,
171        steps,
172        repeat,
173        |next| (*next, *next),
174        |&x| -> Result<Step<T>, E> {
175            let (value, slope) = at(x)?;
176            if arrive == Arrive::AtZero && value.could_be_zero() {
177                return Ok(Step::Done);
178            }
179            let (step, size) = T::step(value, slope)?;
180            if arrive == Arrive::StepsStopShrinking {
181                // A NaN step is no shorter either.
182                if size.partial_cmp(&last) != Some(std::cmp::Ordering::Less) {
183                    return Ok(Step::Done);
184                }
185                last = size;
186            }
187            Ok(Step::Next(x.settle(step, lo, hi)))
188        },
189    )?;
190    let arrived = iterated.end == End::Done || arrive == Arrive::Exhausted;
191    Ok((iterated.seed, arrived))
192}
193
194#[cfg(test)]
195mod tests {
196    /// A step that comes back as everything, as one over a singular Jacobian
197    /// does, leaves the box as it was: refinement only tightens.
198    #[test]
199    fn refining_past_a_singular_step_gives_the_box_back() {
200        let start = [Scalar::new(0.25, 0.75)];
201        let domain = [(Scalar::ZERO, Scalar::ONE)];
202        let refined = super::refine(start, domain, |_| Some([Scalar::ENTIRE]));
203        assert_eq!(refined, start);
204    }
205
206    use super::*;
207
208    /// Each stopping rule ends where it says: at a value that could be
209    /// zero, where the steps stop shrinking, or after every step.
210    #[test]
211    fn newton_1d_arrives_as_asked() {
212        let square = |x: f64| Ok::<_, GeopError>((x * x - 2.0, 2.0 * x));
213        let root = |arrive| newton_1d(1.0, (0.0, 2.0), 50, arrive, square).unwrap();
214        let (shrunk, arrived) = root(Arrive::StepsStopShrinking);
215        assert!(arrived && (shrunk * shrunk - 2.0).abs() <= 4.0 * f64::EPSILON);
216        assert_eq!(root(Arrive::Exhausted), (shrunk, true));
217        // The value at a float is hardly ever exactly zero.
218        let at_zero = newton_1d(3.0, (0.0, 4.0), 50, Arrive::AtZero, |x: f64| {
219            Ok::<_, GeopError>((x - 2.0, 1.0))
220        });
221        assert_eq!(at_zero.unwrap(), (2.0, true));
222    }
223}