geop_core_math/solvers/minimize.rs
1//! Minimizing a merit over a point the caller is free to choose: the
2//! nearest point of a curve or a surface. The point found is a choice, not
3//! an enclosure of a true minimum — what it is chosen for is measured at it
4//! afterwards — so the merit is a plain number.
5
6use crate::{
7 geop_error::GeopError,
8 scalars::Scalar,
9 solvers::probe::{Probe, halve},
10};
11
12/// Damped descent from `start`, at most `steps` steps. `direction(x)`
13/// gives the step from `x` as the point it leads to at a `scale` of the
14/// full step (`None` where there is none to take); a step is taken at the
15/// first of `1, 1/2, 1/4, …` (`halvings` of them) at which it definitely
16/// lowers `merit`, and the descent stops where none does. What is
17/// returned is never worse than `start`.
18pub fn descend<X: Clone, E: From<GeopError>, D: Fn(Scalar) -> X>(
19 start: X,
20 steps: usize,
21 halvings: usize,
22 merit: impl Fn(&X) -> Result<f64, E>,
23 mut direction: impl FnMut(&X) -> Result<Option<D>, E>,
24) -> Result<X, E> {
25 let mut x = start;
26 let mut current = merit(&x)?;
27 for _ in 0..steps {
28 let Some(to) = direction(&x)? else {
29 break;
30 };
31 let lower = halve::<_, E>(Scalar::ONE, halvings, |&scale| {
32 let next = to(scale);
33 let m = merit(&next)?;
34 Ok(if m < current {
35 Probe::Found((next, m))
36 } else {
37 Probe::Next
38 })
39 })?;
40 let Some((next, m)) = lower else {
41 break;
42 };
43 (x, current) = (next, m);
44 }
45 Ok(x)
46}