Skip to main content

geop_core_math/solvers/
least_squares.rs

1//! Levenberg–Marquardt, constrained: minimizing a sum of squared residuals
2//! `Σ r(x)²` among the points where other residuals `c(x)` vanish, from
3//! their values and Jacobians.
4//!
5//! Each step solves the problem linearized at the current point — minimize
6//! `|r + J_r δ|² + δᵀ(B + λD)δ` subject to `c + J_c δ = 0`, or as near to it
7//! as first order gets — by the null-space method. `D` is
8//! Marquardt's scaling, the diagonal of the Jacobians' normal matrix, and
9//! `λ` shrinks after every step taken and grows after every one turned down:
10//! Gauss–Newton near a solution, gradient descent far from one. `B` is what
11//! Gauss–Newton leaves out — the curvature of the constraints, weighted by
12//! how hard the sum pulls against them — estimated from the steps taken (a
13//! symmetric rank-one update, along the constraints only): without it,
14//! wherever the sum cannot reach zero against curved constraints, steps
15//! along them overshoot and the minimizer zig-zags. That curvature is as
16//! often negative as positive — a mate pulled outwards curves the sum down
17//! along it — so `B` is not kept positive, as a BFGS estimate would be:
18//! one that cannot learn a negative curvature left an arm's drags creeping
19//! towards the preferences' minimum until they ran out of steps. Only
20//! `J_rᵀJ_r + B + λD`, over the directions the constraints leave free, must
21//! be positive, and `λ` grows until it is. Where a step it proposed is
22//! turned down, the step is tried again without it, and it is learned
23//! afresh if that does better — a configuration passing through a singular
24//! one changes it abruptly.
25//!
26//! Keeping `c` exact, rather than adding it to the sum with a large weight,
27//! is what makes the sum a pure preference: it can never buy itself a
28//! little lower with a little violation of `c` — which, wherever a long
29//! lever multiplies that violation, is a lot of motion.
30//!
31//! Nor are the two weighed against each other to judge a step. A step is
32//! taken where it definitely lowers either the sum or the constraints'
33//! violation `|c|` — against the point it starts from and against every
34//! point taken before (a filter), so the two cannot trade back and forth
35//! forever. A merit `Σ r² + ρ |c|` would need `ρ` above the multipliers the
36//! sum pulls against the constraints with, and those grow without bound at
37//! a singular configuration — two links in line, say — where the merit's
38//! own width then hides every step that does reduce the violation.
39//!
40//! A variable no residual depends on is never moved, and a step never has a
41//! component along a direction no residual depends on — unlike a
42//! quasi-Newton method on the sum alone, whose curvature estimate grows
43//! without bound along such a direction until rounding noise in the
44//! gradient throws it arbitrarily far.
45//!
46//! Every value is an honest enclosure. The steps themselves are free
47//! choices — any step is a valid seed for the next — so they are sharpened;
48//! whether a step is taken is not, and it is taken only where it
49//! *definitely* improves. So the minimizer needs no tolerance: it stops where
50//! no step can definitely improve on where it is, which is as near the
51//! minimum as the residuals' own uncertainty can tell.
52
53use crate::{
54    geop_error::GeopResult,
55    scalars::Scalar,
56    solvers::linear::{cholesky, solve_cholesky},
57};
58
59/// How long [`minimize`] may try, and how far a step may go.
60#[derive(Clone, Copy, Debug)]
61pub struct Options {
62    /// Steps taken at most: how hard it tries, never what an answer means.
63    pub max_iterations: usize,
64    /// The longest step, but where the steps before landed as their
65    /// linearization said: where the variables have a natural size, no
66    /// step longer than it is sensible until the problem shows it is.
67    pub max_step: Scalar,
68}
69
70/// Where [`minimize`] ended, and how it got there.
71#[derive(Clone, Debug)]
72pub struct Outcome<P> {
73    pub x: P,
74    pub iterations: usize,
75    pub stop: Stop,
76    /// Steps proposed and turned down — or proposed again without the
77    /// curvature learned — on the way: what the iterations do not count.
78    pub turned_down: usize,
79}
80
81/// Why [`minimize`] stopped where it did.
82#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
83pub enum Stop {
84    /// The residuals cannot be evaluated at the start.
85    Infeasible,
86    /// The next step changes no variable at all — after the steps before it
87    /// were turned down, the last one for `rejected`.
88    NoChange { rejected: Option<Rejection> },
89    /// Neither the sum's change nor the violation's can be told from their
90    /// width, and the step no longer shrinks: as near the minimum as can be
91    /// told.
92    Undecided,
93    /// The linearized model expects the next step to lower neither the sum
94    /// nor the violation definitely — with `independent` of the
95    /// `constraints` rows telling it something new.
96    Flat {
97        independent: usize,
98        constraints: usize,
99    },
100    /// No step, however damped, is taken; the last one tried turned down
101    /// for `rejected`.
102    Damped { rejected: Option<Rejection> },
103    /// [`Options::max_iterations`] steps were taken.
104    Budget,
105}
106
107/// Why a step was turned down.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
109pub enum Rejection {
110    /// The damped model's curvature is not positive.
111    NotPositive,
112    /// The residuals cannot be evaluated where it leads.
113    Infeasible,
114    /// Cut so short that the linearized model cannot tell it from no step.
115    Unresolved,
116    /// Where it leads, neither the sum nor the violation is definitely
117    /// lower than at every point taken before.
118    NotLower,
119}
120
121/// Where [`minimize`]'s points are, and how a step moves one: the variables
122/// themselves, a step added to them ([`Euclidean`]) — or a manifold, where
123/// a step is taken from a point along its tangent space (a pose by the
124/// exponential map of a twist, say), and the Jacobians evaluated at a
125/// point are by steps from that point.
126pub trait Space {
127    type Point: Clone;
128
129    /// How many numbers a step from `at` takes.
130    fn dimension(&self, at: &Self::Point) -> usize;
131
132    /// Where `step` takes `at`; `None` where it cannot. The point a step
133    /// leads to is a free choice: sharp.
134    fn step(&self, at: &Self::Point, step: &[Scalar]) -> Option<Self::Point>;
135
136    /// Whether `b`, where a step from `a` led, is no different from `a`:
137    /// the step changed nothing.
138    fn same(&self, a: &Self::Point, b: &Self::Point) -> bool;
139}
140
141/// The variables themselves, as points: a step is added to them.
142pub struct Euclidean;
143
144impl Space for Euclidean {
145    type Point = Vec<Scalar>;
146
147    fn dimension(&self, at: &Vec<Scalar>) -> usize {
148        at.len()
149    }
150
151    fn step(&self, at: &Vec<Scalar>, step: &[Scalar]) -> Option<Vec<Scalar>> {
152        Some(
153            at.iter()
154                .zip(step)
155                .map(|(a, d)| a.add(*d).sharpen())
156                .collect(),
157        )
158    }
159
160    fn same(&self, a: &Vec<Scalar>, b: &Vec<Scalar>) -> bool {
161        a.iter().zip(b).all(|(a, b)| a.could_be_equal(*b))
162    }
163}
164
165/// Residuals, and their Jacobian row by row.
166#[derive(Clone, Debug, Default)]
167pub struct Rows {
168    pub values: Vec<Scalar>,
169    pub jacobian: Vec<Vec<Scalar>>,
170}
171
172impl Rows {
173    pub fn push(&mut self, value: Scalar, row: Vec<Scalar>) {
174        self.values.push(value);
175        self.jacobian.push(row);
176    }
177
178    /// `other`'s rows, after its own.
179    pub fn append(&mut self, other: Rows) {
180        self.values.extend(other.values);
181        self.jacobian.extend(other.jacobian);
182    }
183
184    /// `values + jacobian δ`: the residuals a step `δ` leads to, to first
185    /// order.
186    fn linearized(&self, delta: &[Scalar]) -> Vec<Scalar> {
187        (self.values.iter().zip(&self.jacobian))
188            .map(|(&v, row)| v.add(dot(row, delta)))
189            .collect()
190    }
191
192    /// `|values + jacobian δ|²`: at `δ = 0`, the sum of squares.
193    fn squared(&self, delta: &[Scalar]) -> Scalar {
194        let r = self.linearized(delta);
195        dot(&r, &r)
196    }
197
198    /// `jacobianᵀ w`.
199    fn transposed(&self, w: &[Scalar], n: usize) -> Vec<Scalar> {
200        let mut g = vec![Scalar::ZERO; n];
201        for (&wi, row) in w.iter().zip(&self.jacobian) {
202            for (gi, &ri) in g.iter_mut().zip(row) {
203                *gi = gi.add(ri.mul(wi));
204            }
205        }
206        g
207    }
208}
209
210/// The residuals at a point: those that must vanish, and those whose
211/// squares are minimized.
212#[derive(Clone, Debug, Default)]
213pub struct Evaluation {
214    pub constraints: Rows,
215    pub sum: Rows,
216}
217
218/// By how much `λ` shrinks after a step taken, and grows after one not.
219const SHRINK: i64 = 3;
220const GROW: i64 = 4;
221
222fn dot(a: &[Scalar], b: &[Scalar]) -> Scalar {
223    a.iter()
224        .zip(b)
225        .fold(Scalar::ZERO, |sum, (&x, &y)| sum.add(x.mul(y)))
226}
227
228/// `|v|`, scaled by the largest size among `v` — a free choice, sharp —
229/// before it is squared: the square of a number well below one keeps half
230/// its digits, and in fixed point none at all. Squared as they were, the
231/// residuals of a mate off by `1e-6` had a norm of `[0, 4.6e-5]` in fixed
232/// point, which no step could definitely lower: a body pinned a half turn
233/// away stopped `1e-6` off, ten times the tolerance.
234fn norm(v: &[Scalar]) -> GeopResult<Scalar> {
235    let largest = v.iter().fold(Scalar::ZERO, |m, x| m.max(x.abs().upper()));
236    if !largest.definitely_greater(Scalar::ZERO) {
237        return dot(v, v).sqrt();
238    }
239    let scaled = (v.iter().map(|x| x.div(largest))).collect::<GeopResult<Vec<Scalar>>>()?;
240    Ok(largest.mul(dot(&scaled, &scaled).sqrt()?))
241}
242
243/// A value of the model a step is computed from — a slope, a residual, a
244/// curvature, the step itself — chosen from its enclosure: any value in it
245/// serves (see the module docs), so zero where zero is among them, else
246/// the middle. A slope that could be zero is rounding around an exact
247/// zero, and turned into a small number it would couple what does not
248/// depend on each other — a coupling a weakly held variable then follows
249/// arbitrarily far.
250fn choose(v: Scalar) -> Scalar {
251    if v.could_be_equal(Scalar::ZERO) {
252        Scalar::ZERO
253    } else {
254        v.sharpen()
255    }
256}
257
258fn sharp(v: &[Scalar]) -> Vec<Scalar> {
259    v.iter().map(|&x| choose(x)).collect()
260}
261
262/// A step of the linearized problem (see [`step`]).
263struct Step {
264    delta: Vec<Scalar>,
265    /// The constraints' multipliers.
266    lambda: Vec<Scalar>,
267    /// How many of the constraints' rows say something the others do not.
268    independent: usize,
269}
270
271/// `v - a w`, chosen (see [`choose`]).
272fn minus_scaled(v: &[Scalar], a: Scalar, w: &[Scalar]) -> Vec<Scalar> {
273    v.iter()
274        .zip(w)
275        .map(|(&x, &y)| choose(x.sub(a.mul(y))))
276        .collect()
277}
278
279/// The step of the problem linearized at `e`, with curvature `b` and
280/// damping `mu`, sharpened — and the constraints' multipliers. `None` if
281/// the damped curvature is not positive along the directions the
282/// constraints leave free, among the variables `moving`.
283///
284/// By the null-space method: an orthonormal basis `Q` of the constraints'
285/// rows, and one `Z` of the directions they leave free. The step is the
286/// shortest one meeting the linearized constraints, `δ_c = Q y` with
287/// `R y = -c` for the rows' coefficients `R` in `Q` — no curvature in it at
288/// all — plus the one along `Z` that minimizes the model there, from the
289/// curvature reduced to those directions, `Zᵀ H Z`. Unlike a Schur
290/// complement `J_c H⁻¹ J_cᵀ`, nothing inverts `H` along a direction it
291/// barely curves in — a variable only the damping holds, say, which the
292/// constraints pin — where the inverse is huge and the complement loses as
293/// many digits as it is large.
294///
295/// A row says nothing the rows before it do not where what is left of it,
296/// after taking out its component along their basis, could be zero — asked
297/// of the row's honest enclosure: a question about the constraints, not a
298/// choice. Everything else is the model's own choice, so sharp.
299fn step(
300    e: &Evaluation,
301    b: &[Vec<Scalar>],
302    moving: &[bool],
303    mu: Scalar,
304) -> GeopResult<Option<Step>> {
305    let n = moving.len();
306    let keep: Vec<usize> = (0..n).filter(|&i| moving[i]).collect();
307    let k = keep.len();
308    let squeeze = |v: &[Scalar]| -> Vec<Scalar> { keep.iter().map(|&i| v[i]).collect() };
309    let jr: Vec<Vec<Scalar>> = e.sum.jacobian.iter().map(|r| sharp(&squeeze(r))).collect();
310    let jc_wide: Vec<Vec<Scalar>> = e.constraints.jacobian.iter().map(|r| squeeze(r)).collect();
311    let jc: Vec<Vec<Scalar>> = jc_wide.iter().map(|r| sharp(r)).collect();
312    let c = sharp(&e.constraints.values);
313    let m = jc.len();
314    super::factorised(jr.len() + m, k);
315
316    // H = J_rᵀJ_r + B + λ D, over the variables that move.
317    let mut h: Vec<Vec<Scalar>> = keep.iter().map(|&i| squeeze(&b[i])).collect();
318    for row in &jr {
319        for i in 0..k {
320            for j in 0..k {
321                h[i][j] = h[i][j].add(row[i].mul(row[j]));
322            }
323        }
324    }
325    for (i, row) in h.iter_mut().enumerate() {
326        let d = jr
327            .iter()
328            .chain(&jc)
329            .fold(Scalar::ZERO, |d, r| d.add(r[i].mul(r[i])));
330        row[i] = row[i].add(mu.mul(d));
331        row.iter_mut().for_each(|v| *v = choose(*v));
332    }
333    // Damped so hard that the curvature is no finite number: no step.
334    if !h.iter().flatten().all(|v| v.is_finite()) {
335        return Ok(None);
336    }
337
338    // Q, and each independent row's coefficients in it (modified
339    // Gram–Schmidt): row `i` is `Σ_j R[i][j] q_j`, `R[i]` ending in its own
340    // `q`'s.
341    // Whether a row is independent is asked honestly: what is left of its
342    // enclosure outside the span of the rows before — their sharp basis, not
343    // exactly orthonormal, so projected onto through its Gram matrix `QᵀQ`
344    // (enclosed, and close to the identity, so the projection stays tight).
345    // What a sharp basis leaves of a row that depends on the others is
346    // rounding, not zero; what this leaves of it could be zero.
347    let mut q: Vec<Vec<Scalar>> = Vec::new();
348    // The Cholesky factor of the Gram matrix, row by row.
349    let mut gram: Vec<Vec<Scalar>> = Vec::new();
350    let mut rows: Vec<(usize, Vec<Scalar>)> = Vec::new();
351    let mut dependent: Vec<(usize, Vec<Scalar>)> = Vec::new();
352    for i in 0..m {
353        let r = q.len();
354        let b: Vec<Scalar> = q.iter().map(|qj| dot(&jc_wide[i], qj)).collect();
355        let a = solve_cholesky(&gram, &vec![false; r], &b)?;
356        let outside: Vec<Scalar> = (0..k)
357            .map(|p| (0..r).fold(jc_wide[i][p], |sum, j| sum.sub(a[j].mul(q[j][p]))))
358            .collect();
359        if !norm(&outside)?.definitely_greater(Scalar::ZERO) {
360            // Its coefficients in the basis, for the least-squares
361            // correction below.
362            dependent.push((i, sharp(&a)));
363            continue;
364        }
365        let mut v = jc[i].clone();
366        let mut coefficients = Vec::new();
367        for qj in &q {
368            let a = choose(dot(&v, qj));
369            v = minus_scaled(&v, a, qj);
370            coefficients.push(a);
371        }
372        let length = norm(&v)?.sharpen();
373        coefficients.push(length);
374        let q_new: Vec<Scalar> = v
375            .iter()
376            .map(|&x| choose(x.div(length).unwrap_or(Scalar::ZERO)))
377            .collect();
378        let mut row = vec![Scalar::ZERO; r + 1];
379        for j in 0..r {
380            row[j] = (0..j)
381                .fold(dot(&q_new, &q[j]), |sum, k| sum.sub(row[k].mul(gram[j][k])))
382                .div(gram[j][j])?;
383        }
384        row[r] = (0..r)
385            .fold(dot(&q_new, &q_new), |sum, k| sum.sub(row[k].mul(row[k])))
386            .sqrt()?;
387        gram.push(row);
388        q.push(q_new);
389        rows.push((i, coefficients));
390    }
391    let r = q.len();
392
393    // δ_c = Q y, the least-squares correction of every row — `|c + J_c Q
394    // y|` least — by Householder reflections of the rows' coefficients in
395    // Q, not their normal equations, which square how ill-conditioned they
396    // are: exactly `R y = -c` where the dependent rows agree with the
397    // others, and as near as first order gets where they do not. Squared, a
398    // notched outline's trimmed offset — two arcs held to one crossing by a
399    // tangency each — gave a step that met none of the rows it was solved
400    // for, and the solve stopped flat at its first.
401    let coefficients_of = |coefficients: &[Scalar]| -> Vec<Scalar> {
402        (0..r)
403            .map(|j| coefficients.get(j).copied().unwrap_or(Scalar::ZERO))
404            .collect()
405    };
406    let all: Vec<(usize, Vec<Scalar>)> = rows
407        .iter()
408        .chain(&dependent)
409        .map(|(i, coefficients)| (*i, coefficients_of(coefficients)))
410        .collect();
411    let rhs: Vec<Scalar> = all.iter().map(|(i, _)| c[*i].neg()).collect();
412    let y = sharp(&least_squares(
413        all.into_iter().map(|(_, row)| row).collect(),
414        rhs,
415    )?);
416    let mut delta_c = vec![Scalar::ZERO; k];
417    for (t, qt) in q.iter().enumerate() {
418        delta_c = minus_scaled(&delta_c, y[t].neg(), qt);
419    }
420
421    // Z: the unit directions with what is left of them after Q, taken one
422    // at a time by the largest remainder — orthonormalized against Q and
423    // each other.
424    let mut left: Vec<Vec<Scalar>> = (0..k)
425        .map(|p| {
426            let mut v = vec![Scalar::ZERO; k];
427            v[p] = Scalar::ONE;
428            for qt in &q {
429                v = minus_scaled(&v, choose(dot(&v, qt)), qt);
430            }
431            v
432        })
433        .collect();
434    let mut z: Vec<Vec<Scalar>> = Vec::new();
435    while z.len() < k - r {
436        let lengths: Vec<Scalar> = left.iter().map(|v| dot(v, v)).collect();
437        let Some(best) = (0..k).reduce(|a, b| {
438            if lengths[b].definitely_greater(lengths[a]) {
439                b
440            } else {
441                a
442            }
443        }) else {
444            break;
445        };
446        let length = lengths[best].sqrt()?.sharpen();
447        if !length.definitely_greater(Scalar::ZERO) {
448            break;
449        }
450        let zt: Vec<Scalar> = left[best]
451            .iter()
452            .map(|&x| choose(x.div(length).unwrap_or(Scalar::ZERO)))
453            .collect();
454        for v in &mut left {
455            *v = minus_scaled(v, choose(dot(v, &zt)), &zt);
456        }
457        z.push(zt);
458    }
459
460    // Along Z: (Zᵀ H Z) w = -Zᵀ (g + H δ_c).
461    let g = sharp(&squeeze(&e.sum.transposed(&sharp(&e.sum.values), n)));
462    let h_times =
463        |v: &[Scalar]| -> Vec<Scalar> { h.iter().map(|row| choose(dot(row, v))).collect() };
464    let force: Vec<Scalar> = g
465        .iter()
466        .zip(h_times(&delta_c))
467        .map(|(&a, b)| choose(a.add(b)))
468        .collect();
469    let hz: Vec<Vec<Scalar>> = z.iter().map(|zt| h_times(zt)).collect();
470    let reduced: Vec<Vec<Scalar>> = z
471        .iter()
472        .map(|zi| hz.iter().map(|hzj| choose(dot(zi, hzj))).collect())
473        .collect();
474    let (l, not_positive) = cholesky(&reduced)?;
475    if not_positive.contains(&true) {
476        return Ok(None);
477    }
478    let rhs: Vec<Scalar> = z.iter().map(|zt| choose(dot(zt, &force).neg())).collect();
479    let w = sharp(&solve_cholesky(&l, &not_positive, &rhs)?);
480    let mut kept = delta_c;
481    for (t, zt) in z.iter().enumerate() {
482        kept = minus_scaled(&kept, w[t].neg(), zt);
483    }
484
485    // The multipliers: J_cᵀ λ = -(g + H δ), R ᵀ λ = Qᵀ (-(g + H δ)).
486    let pull: Vec<Scalar> = g
487        .iter()
488        .zip(h_times(&kept))
489        .map(|(&a, b)| choose(a.add(b).neg()))
490        .collect();
491    // A curvature positive by so little that the step it solves for, or
492    // the force along it, is no finite number is as good as not positive.
493    if !kept.iter().chain(&pull).all(|v| v.is_finite()) {
494        return Ok(None);
495    }
496    let mut lambda_rows = vec![Scalar::ZERO; r];
497    for t in (0..r).rev() {
498        let known = (t + 1..r).fold(dot(&q[t], &pull), |sum, s| {
499            sum.sub(rows[s].1[t].mul(lambda_rows[s]))
500        });
501        lambda_rows[t] = choose(known.div(rows[t].1[t])?);
502    }
503    let mut lambda = vec![Scalar::ZERO; m];
504    for (t, (i, _)) in rows.iter().enumerate() {
505        lambda[*i] = lambda_rows[t];
506    }
507    let mut delta = vec![Scalar::ZERO; n];
508    for (p, &i) in keep.iter().enumerate() {
509        delta[i] = kept[p];
510    }
511    Ok(Some(Step {
512        delta,
513        lambda,
514        independent: r,
515    }))
516}
517
518/// Minimizes `Σ r(x)²` from `x0` among the points where `c(x) = 0`, with
519/// `evaluate` giving both and their Jacobians — by steps from the point,
520/// in `space` — and `None` where they cannot be evaluated, which makes the
521/// point infeasible. Where the constraints cannot all hold, it ends where
522/// they come closest. Always returns the best point found; whether it is
523/// good enough is the caller's question.
524///
525/// `evaluate` is awaited: a caller running many minimizations side by side
526/// answers all their evaluations at once (see
527/// [`crate::solvers::system`]'s groups).
528pub async fn minimize<P: Space>(
529    space: &P,
530    evaluate: impl AsyncFn(&P::Point) -> Option<Evaluation>,
531    x0: P::Point,
532    options: Options,
533) -> GeopResult<Outcome<P::Point>> {
534    let n = space.dimension(&x0);
535    let mut x = x0;
536    let Some(mut e) = evaluate(&x).await else {
537        return Ok(Outcome {
538            x,
539            iterations: 0,
540            stop: Stop::Infeasible,
541            turned_down: 0,
542        });
543    };
544    // A variable moves only if some residual definitely depends on it: a
545    // slope that could be zero — rounding around an exact zero — says
546    // nothing about which way to move it, only how far a step along it
547    // could go unchecked.
548    let moving: Vec<bool> = (0..n)
549        .map(|i| {
550            e.sum
551                .jacobian
552                .iter()
553                .chain(&e.constraints.jacobian)
554                .any(|row| row[i].definitely_not_equal(Scalar::ZERO))
555        })
556        .collect();
557    // The sum and the violation at every point taken: a step must
558    // definitely improve on each in one of the two.
559    let mut filter: Vec<(Scalar, Scalar)> = Vec::new();
560    let mut mu = Scalar::from_ratio(1, 1000)?;
561    let mut turned_down = 0;
562    let mut b = vec![vec![Scalar::ZERO; n]; n];
563    // The length of the last step taken.
564    let mut last: Option<Scalar> = None;
565    // How far a step may go: what bounds a step along the constraints, which
566    // `λ` does not damp — their linearization holds only so far.
567    let mut radius = options.max_step;
568    for iteration in 0..options.max_iterations {
569        let (f, theta) = (e.sum.squared(&[]), norm(&e.constraints.values)?);
570        let mut first = true;
571        let mut rejected = None;
572        // Whether this iteration's steps leave out the curvature learned
573        // (see below).
574        let mut plain = false;
575        let no_curvature = vec![vec![Scalar::ZERO; n]; n];
576        let accepted = loop {
577            let curvature = if plain { &no_curvature } else { &b };
578            if let Some(Step {
579                mut delta,
580                lambda,
581                independent,
582            }) = step(&e, curvature, &moving, mu)?
583            {
584                // What the linearized model expects of a step: where its own
585                // step expects no definite drop in either, no smaller step
586                // makes one — as near the minimum as can be told, or, damped
587                // already, no step helps — and no point is evaluated to
588                // learn it. The model of the sum is the one the step was
589                // computed from: its rows' linearization, and the curvature
590                // `B` it was computed with — what the constraints' curvature
591                // costs or gains a step along them. Without `B`, a step that
592                // `B` lengthened where the constraints curve the sum down
593                // looked to the rows alone like no drop at all: a gear pair
594                // dragged a quarter turn stopped, flat, at 36°.
595                let expects = |delta: &[Scalar]| -> GeopResult<bool> {
596                    let bent = (0..n).fold(Scalar::ZERO, |sum, i| {
597                        sum.add(delta[i].mul(dot(&curvature[i], delta)))
598                    });
599                    Ok(f.sub(e.sum.squared(delta))
600                        .sub(bent)
601                        .definitely_greater(Scalar::ZERO)
602                        || theta
603                            .sub(norm(&e.constraints.linearized(delta))?)
604                            .definitely_greater(Scalar::ZERO))
605                };
606                if !expects(&delta)? {
607                    return Ok(Outcome {
608                        x,
609                        iterations: iteration,
610                        stop: if first {
611                            Stop::Flat {
612                                independent,
613                                constraints: e.constraints.values.len(),
614                            }
615                        } else {
616                            Stop::Damped { rejected }
617                        },
618                        turned_down,
619                    });
620                }
621                let length = norm(&delta)?;
622                if length.definitely_greater(radius) {
623                    let shrink = radius.div(length)?;
624                    delta.iter_mut().for_each(|d| *d = choose(d.mul(shrink)));
625                }
626                // Cut so short that not even the model can tell it from no
627                // step at all, the cut is below what the arithmetic resolves:
628                // it is lifted again.
629                if !expects(&delta)? {
630                    radius = options.max_step;
631                    rejected = Some(Rejection::Unresolved);
632                } else {
633                    let delta = sharp(&delta);
634                    // What the linearization expects of it: the violation
635                    // where it leads.
636                    let predicted = norm(&e.constraints.linearized(&delta))?;
637                    let next = space.step(&x, &delta);
638                    // Where not even this step changes anything, no smaller one
639                    // will: nothing improves on here.
640                    if next.as_ref().is_some_and(|next| space.same(next, &x)) {
641                        return Ok(Outcome {
642                            x,
643                            iterations: iteration,
644                            stop: Stop::NoChange { rejected },
645                            turned_down,
646                        });
647                    }
648                    // How far the step went, before any correction: what
649                    // the radius shrinks from where it is turned down.
650                    let tried = norm(&delta)?;
651                    let landed = match next {
652                        Some(next) => match evaluate(&next).await {
653                            Some(e_next) => Some(
654                                corrected(space, &evaluate, &moving, theta, next, e_next, delta)
655                                    .await?,
656                            ),
657                            None => None,
658                        },
659                        None => None,
660                    };
661                    if let Some(landed) = landed {
662                        let judged = judge(&e, &landed.1, theta, &filter, last, landed.2)?;
663                        if first && judged.undecided && last.is_some() && !judged.contracting {
664                            return Ok(Outcome {
665                                x,
666                                iterations: iteration,
667                                stop: Stop::Undecided,
668                                turned_down,
669                            });
670                        }
671                        let taken = |j: &Judged| j.lower || (first && j.undecided && j.contracting);
672                        if taken(&judged) {
673                            let (next, e_next, _) = landed;
674                            // Whether the step brought the constraints as near
675                            // as their linearization said it would — three
676                            // quarters of the drop it predicted, at least:
677                            // the linearization then
678                            // holds that far, and `max_step` does not bound
679                            // the next. A part mated from far away is
680                            // brought there in a few steps, not one of its
681                            // sizes at a time.
682                            let held = |old: Scalar, predicted: Scalar, new: Scalar| {
683                                let gained = old.sub(new).mul(Scalar::from_i64(4));
684                                gained
685                                    .definitely_greater(old.sub(predicted).mul(Scalar::from_i64(3)))
686                            };
687                            let linear = theta.definitely_greater(Scalar::ZERO)
688                                && held(theta, predicted, norm(&e_next.constraints.values)?);
689                            let length = judged.length;
690                            // Never to zero, which the scalar's resolution
691                            // rounds a small one to in fixed point: no
692                            // growth brings it back, and a step turned down
693                            // is then tried again as it was, forever.
694                            let shrunk = mu.div(Scalar::from_i64(SHRINK))?.sharpen();
695                            if shrunk.definitely_greater(Scalar::ZERO) {
696                                mu = shrunk;
697                            }
698                            let reach = Scalar::TWO.mul(length);
699                            if reach.definitely_greater(radius) {
700                                radius = if linear || reach.definitely_less(options.max_step) {
701                                    reach.upper()
702                                } else {
703                                    options.max_step
704                                };
705                            }
706                            // Without the curvature learned, where that did better:
707                            // it is learned afresh from here.
708                            if plain {
709                                b.iter_mut().flatten().for_each(|v| *v = Scalar::ZERO);
710                            }
711                            update_curvature(&mut b, &judged.s, &e, &e_next, &lambda)?;
712                            filter.push((f, theta));
713                            last = Some(length);
714                            break Some((next, e_next));
715                        }
716                        rejected = Some(Rejection::NotLower);
717                        // The curvature learned from earlier steps proposed this
718                        // one. Where it fails, that curvature may no longer
719                        // hold — a configuration passing through a singular
720                        // one changes it abruptly — so the step is first tried
721                        // again without it, as it stands, before damping it.
722                        if !plain && learned(&b) {
723                            plain = true;
724                            turned_down += 1;
725                            continue;
726                        }
727                    } else {
728                        rejected = Some(Rejection::Infeasible);
729                    }
730                    radius = tried.div(Scalar::from_i64(GROW))?.sharpen();
731                }
732            } else {
733                rejected = Some(Rejection::NotPositive);
734            }
735            first = false;
736            turned_down += 1;
737            mu = mu.mul(Scalar::from_i64(GROW)).sharpen();
738            if !mu.is_finite() {
739                break None;
740            }
741        };
742        let Some((next, e_next)) = accepted else {
743            return Ok(Outcome {
744                x,
745                iterations: iteration,
746                stop: Stop::Damped { rejected },
747                turned_down,
748            });
749        };
750        (x, e) = (next, e_next);
751    }
752    Ok(Outcome {
753        x,
754        iterations: options.max_iterations,
755        stop: Stop::Budget,
756        turned_down,
757    })
758}
759
760/// How the step `s` from a point (evaluated as `e`) to the next (as
761/// `e_next`) fares — see [`minimize`], where it is taken or turned down on
762/// this.
763struct Judged {
764    /// The step, and its length.
765    s: Vec<Scalar>,
766    length: Scalar,
767    /// It definitely improves on `x`, and is not worse in both than any
768    /// point taken before.
769    lower: bool,
770    /// Neither the sum's change nor the violation's can be told from their
771    /// width.
772    undecided: bool,
773    /// It is at most half the last step taken.
774    contracting: bool,
775}
776
777fn judge(
778    e: &Evaluation,
779    e_next: &Evaluation,
780    theta: Scalar,
781    filter: &[(Scalar, Scalar)],
782    last: Option<Scalar>,
783    s: Vec<Scalar>,
784) -> GeopResult<Judged> {
785    let length = norm(&s)?;
786    // `Σ r² - Σ r'²` summed as `Σ (r - r')(r + r')`: near the minimum of a
787    // sum that stays large, the difference of the two sums is lost to their
788    // width.
789    let lower_sum = e
790        .sum
791        .values
792        .iter()
793        .zip(&e_next.sum.values)
794        .fold(Scalar::ZERO, |sum, (&r, &r_next)| {
795            sum.add(r.sub(r_next).mul(r.add(r_next)))
796        });
797    let theta_next = norm(&e_next.constraints.values)?;
798    let lower_violation = theta.sub(theta_next);
799    let f_next = e_next.sum.squared(&[]);
800    // Far from the minimum, a step is taken where it definitely improves on
801    // here, and is not worse in both than any point taken before. Near it,
802    // both changes drown in their own width, and what still tells progress
803    // is Newton's: a step at most half the last one taken contracts towards
804    // a solution — until rounding stops it shrinking, which is where it
805    // ends. Shrinking any less is no contraction: steps that only barely
806    // shrink add up to any distance at all.
807    let improves = lower_sum.definitely_greater(Scalar::ZERO)
808        || lower_violation.definitely_greater(Scalar::ZERO);
809    let lower = improves
810        && filter.iter().all(|&(f_taken, theta_taken)| {
811            f_next.definitely_less(f_taken) || theta_next.definitely_less(theta_taken)
812        });
813    let undecided = [lower_sum, lower_violation].iter().all(|change| {
814        !change.definitely_greater(Scalar::ZERO) && !change.definitely_less(Scalar::ZERO)
815    });
816    let contracting = last.is_some_and(|last| Scalar::TWO.mul(length).definitely_less(last));
817    Ok(Judged {
818        s,
819        length,
820        lower,
821        undecided,
822        contracting,
823    })
824}
825
826/// Corrections of a step back onto the constraints, at most: each a Newton
827/// step on them alone, which squares how far off they are — how hard a
828/// step is corrected, never whether it is taken.
829const CORRECTIONS: usize = 4;
830
831/// The point `x` (with its evaluation `e`), where the step `taken` from a
832/// point off the constraints by `before` landed, moved back onto them if
833/// the step left them further off — the shortest moves among the variables
834/// `moving` that meet their linearization there, as long as each definitely
835/// lowers their violation (a second-order correction) — and the step, with
836/// the corrections, all told.
837///
838/// A step meets the constraints' linearization where it starts, so it
839/// leaves them by as much as they curve over its length: a body turned by a
840/// large step along the directions they leave free is off them by that
841/// much. Judged there, the filter took steps that lowered the sum while the
842/// violation grew, and the minimizer zig-zagged along the constraints — a
843/// mated arm wandered off its mates by whole lengths and ran out of steps.
844/// Corrected, such a step is judged where it actually leads along them.
845/// A step that brought the constraints nearer is left as it is: the
846/// shortest moves are not the ones the sum prefers, and correcting every
847/// step drifted a sketch's line off where it was.
848async fn corrected<P: Space>(
849    space: &P,
850    evaluate: &impl AsyncFn(&P::Point) -> Option<Evaluation>,
851    moving: &[bool],
852    before: Scalar,
853    mut x: P::Point,
854    mut e: Evaluation,
855    mut taken: Vec<Scalar>,
856) -> GeopResult<(P::Point, Evaluation, Vec<Scalar>)> {
857    if !norm(&e.constraints.values)?.definitely_greater(before) {
858        return Ok((x, e, taken));
859    }
860    for _ in 0..CORRECTIONS {
861        let theta = norm(&e.constraints.values)?;
862        // δ = -J_cᵀ k with J_c J_cᵀ k = c: the shortest step meeting the
863        // linearized constraints, rows that say nothing new left out.
864        let jc: Vec<Vec<Scalar>> = e
865            .constraints
866            .jacobian
867            .iter()
868            .map(|row| {
869                row.iter()
870                    .zip(moving)
871                    .map(|(&v, &m)| if m { choose(v) } else { Scalar::ZERO })
872                    .collect()
873            })
874            .collect();
875        let zero = vec![Scalar::ZERO; taken.len()];
876        let d: Vec<Scalar> = (minus_solved(&jc, &sharp(&e.constraints.values), &zero)?.into_iter())
877            .map(Scalar::sharpen)
878            .collect();
879        if !d.iter().all(|v| v.is_finite()) {
880            break;
881        }
882        let Some(next) = space.step(&x, &d) else {
883            break;
884        };
885        if space.same(&next, &x) {
886            break;
887        }
888        let Some(e_next) = evaluate(&next).await else {
889            break;
890        };
891        if !norm(&e_next.constraints.values)?.definitely_less(theta) {
892            break;
893        }
894        // The steps add up as they would among the variables themselves:
895        // exactly so there, and to first order on a manifold.
896        taken = taken
897            .iter()
898            .zip(&d)
899            .map(|(a, b)| a.add(*b).sharpen())
900            .collect();
901        (x, e) = (next, e_next);
902    }
903    Ok((x, e, taken))
904}
905
906/// Whether any curvature has been learned into `b`.
907fn learned(b: &[Vec<Scalar>]) -> bool {
908    b.iter()
909        .flatten()
910        .any(|v| !v.is_sharp() || v.definitely_not_equal(Scalar::ZERO))
911}
912
913/// The part of `v` along the constraints — orthogonal to the rows of their
914/// Jacobian, `v - J_cᵀ (J_c J_cᵀ)⁻¹ J_c v` — chosen (see [`choose`]); rows
915/// that say nothing the others do not left out.
916fn along(constraints: &Rows, v: &[Scalar]) -> GeopResult<Vec<Scalar>> {
917    let jc: Vec<Vec<Scalar>> = constraints.jacobian.iter().map(|r| sharp(r)).collect();
918    let rhs: Vec<Scalar> = jc.iter().map(|row| dot(row, v)).collect();
919    minus_solved(&jc, &rhs, v)
920}
921
922/// `from - J_cᵀ k` with `J_c J_cᵀ k = rhs`, chosen (see [`choose`]); rows
923/// of `jc` that say nothing the others do not left out.
924fn minus_solved(jc: &[Vec<Scalar>], rhs: &[Scalar], from: &[Scalar]) -> GeopResult<Vec<Scalar>> {
925    let gram: Vec<Vec<Scalar>> = jc
926        .iter()
927        .map(|a| jc.iter().map(|b| dot(a, b)).collect())
928        .collect();
929    let (l, dependent) = cholesky(&gram)?;
930    let k = solve_cholesky(&l, &dependent, rhs)?;
931    Ok((0..from.len())
932        .map(|i| choose((0..jc.len()).fold(from[i], |sum, r| sum.sub(jc[r][i].mul(k[r])))))
933        .collect())
934}
935
936/// Updates `b`, the curvature Gauss–Newton leaves out, from the step `s`
937/// from `e` to `e_next`: by how much the gradient of the Lagrangian
938/// `½ Σ r² + λᵀc` turned along it, beyond what `J_rᵀJ_r` explains (a
939/// structured symmetric rank-one update): `b` then turns the gradient along
940/// `s` exactly as it did, curving up or down. Skipped where the correction
941/// could be square to `s`, which leaves its size undetermined.
942fn update_curvature(
943    b: &mut [Vec<Scalar>],
944    s: &[Scalar],
945    e: &Evaluation,
946    e_next: &Evaluation,
947    lambda: &[Scalar],
948) -> GeopResult<()> {
949    let n = s.len();
950    let r_next = sharp(&e_next.sum.values);
951    let constraints_next = e_next.constraints.transposed(lambda, n);
952    let constraints_before = e.constraints.transposed(lambda, n);
953    let sum_next = e_next.sum.transposed(&r_next, n);
954    let sum_before = e.sum.transposed(&r_next, n);
955    let y: Vec<Scalar> = (0..n)
956        .map(|i| {
957            constraints_next[i]
958                .sub(constraints_before[i])
959                .add(sum_next[i])
960                .sub(sum_before[i])
961        })
962        .collect();
963    // Only along the constraints: off them, the constraints decide where a
964    // step goes, so a curvature there means nothing — and a large one, along
965    // a variable they hold outright, would only skew their solve.
966    let s = along(&e.constraints, s)?;
967    let bs: Vec<Scalar> = (0..n).map(|i| dot(&b[i], &s)).collect();
968    // `(y - B s)·s`, asked of `y`'s honest enclosure — the same along the
969    // constraints as off them, `s` being along them. Where it could be
970    // zero, the correction is square to the step as far as the gradients
971    // tell, and dividing by it would make the curvature whatever their
972    // rounding says.
973    if !dot(&y, &s)
974        .sub(dot(&bs, &s))
975        .definitely_not_equal(Scalar::ZERO)
976    {
977        return Ok(());
978    }
979    let y = along(&e.constraints, &y)?;
980    let v: Vec<Scalar> = (0..n).map(|i| choose(y[i].sub(bs[i]))).collect();
981    let vs = choose(dot(&v, &s));
982    if !vs.definitely_not_equal(Scalar::ZERO) {
983        return Ok(());
984    }
985    for i in 0..n {
986        for j in 0..n {
987            b[i][j] = choose(b[i][j].add(v[i].mul(v[j]).div(vs)?));
988        }
989    }
990    Ok(())
991}
992
993/// The `y` with `|rows y - rhs|` least, by Householder reflections, each
994/// value chosen (see [`choose`]): the model's own numbers. A column with
995/// nothing left of it, after the reflections before, is left out — zero in
996/// `y` — as a pivot that could be zero is.
997fn least_squares(rows: Vec<Vec<Scalar>>, rhs: Vec<Scalar>) -> GeopResult<Vec<Scalar>> {
998    let m = rows.len();
999    let r = rows.first().map_or(0, Vec::len);
1000    // By columns, the right-hand side the last: each reflection is applied
1001    // to every column from the diagonal's on.
1002    let mut columns: Vec<Vec<Scalar>> = (0..r)
1003        .map(|j| rows.iter().map(|row| row[j]).collect())
1004        .collect();
1005    columns.push(rhs);
1006    let mut left_out = vec![false; r];
1007    for j in 0..r.min(m) {
1008        let length = norm(&columns[j][j..])?;
1009        if !length.definitely_greater(Scalar::ZERO) {
1010            left_out[j] = true;
1011            continue;
1012        }
1013        // The reflection taking the column below the diagonal onto it,
1014        // `v = x + sign(x₀) |x| e₀`, away from cancelling.
1015        let alpha = if columns[j][j].definitely_less(Scalar::ZERO) {
1016            length
1017        } else {
1018            length.neg()
1019        };
1020        let mut v = columns[j][j..].to_vec();
1021        v[0] = choose(v[0].sub(alpha));
1022        let vv = choose(dot(&v, &v));
1023        if !vv.definitely_greater(Scalar::ZERO) {
1024            continue;
1025        }
1026        for column in &mut columns[j..] {
1027            let f = dot(&v, &column[j..]).mul(Scalar::TWO).div(vv)?;
1028            for (x, &vt) in column[j..].iter_mut().zip(&v) {
1029                *x = choose(x.sub(f.mul(vt)));
1030            }
1031        }
1032    }
1033    let rhs = &columns[r];
1034    let mut y = vec![Scalar::ZERO; r];
1035    for j in (0..r.min(m)).rev() {
1036        if left_out[j] || !columns[j][j].definitely_not_equal(Scalar::ZERO) {
1037            continue;
1038        }
1039        let known = (j + 1..r).fold(rhs[j], |sum, t| sum.sub(columns[t][j].mul(y[t])));
1040        y[j] = choose(known.div(columns[j][j])?);
1041    }
1042    Ok(y)
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047    use super::*;
1048
1049    /// [`super::minimize`], every evaluation answered as it is asked.
1050    fn minimize<P: Space>(
1051        space: &P,
1052        evaluate: impl Fn(&P::Point) -> Option<Evaluation>,
1053        x0: P::Point,
1054        options: Options,
1055    ) -> GeopResult<Outcome<P::Point>> {
1056        let work = std::pin::pin!(super::minimize(
1057            space,
1058            async |x: &P::Point| evaluate(x),
1059            x0,
1060            options
1061        ));
1062        let mut context = std::task::Context::from_waker(std::task::Waker::noop());
1063        match std::future::Future::poll(work, &mut context) {
1064            std::task::Poll::Ready(outcome) => outcome,
1065            std::task::Poll::Pending => panic!("nothing awaited is pending"),
1066        }
1067    }
1068
1069    fn s(v: f64) -> Scalar {
1070        Scalar::from_f64(v)
1071    }
1072
1073    fn options() -> Options {
1074        Options {
1075            max_iterations: 500,
1076            max_step: s(10.0),
1077        }
1078    }
1079
1080    fn rows(values: Vec<Scalar>, jacobian: Vec<Vec<Scalar>>) -> Rows {
1081        Rows { values, jacobian }
1082    }
1083
1084    fn at(outcome: &Outcome<Vec<Scalar>>) -> Vec<f64> {
1085        outcome.x.iter().map(|v| v.to_f64()).collect()
1086    }
1087
1088    /// Rosenbrock, as residuals `(1 - a, 10 (b - a²))`.
1089    #[test]
1090    fn minimizes_rosenbrock() {
1091        let outcome = minimize(
1092            &Euclidean,
1093            |x: &Vec<Scalar>| {
1094                let (a, b) = (x[0], x[1]);
1095                Some(Evaluation {
1096                    constraints: Rows::default(),
1097                    sum: rows(
1098                        vec![Scalar::ONE.sub(a), s(10.0).mul(b.sub(a.mul(a)))],
1099                        vec![vec![s(-1.0), Scalar::ZERO], vec![s(-20.0).mul(a), s(10.0)]],
1100                    ),
1101                })
1102            },
1103            vec![s(-1.2), s(1.0)],
1104            options(),
1105        )
1106        .unwrap();
1107        let x = at(&outcome);
1108        assert!(
1109            (x[0] - 1.0).abs() < 1e-12 && (x[1] - 1.0).abs() < 1e-12,
1110            "{outcome:?}"
1111        );
1112    }
1113
1114    /// A variable no residual depends on is not moved at all.
1115    #[test]
1116    fn free_variables_stay_put() {
1117        let outcome = minimize(
1118            &Euclidean,
1119            |x: &Vec<Scalar>| {
1120                Some(Evaluation {
1121                    constraints: rows(
1122                        vec![x[0].sub(s(2.0))],
1123                        vec![vec![Scalar::ONE, Scalar::ZERO]],
1124                    ),
1125                    sum: Rows::default(),
1126                })
1127            },
1128            vec![s(0.0), s(0.7)],
1129            options(),
1130        )
1131        .unwrap();
1132        let x = at(&outcome);
1133        assert!((x[0] - 2.0).abs() < 1e-15, "{outcome:?}");
1134        assert_eq!(x[1], 0.7);
1135    }
1136
1137    /// The point of the unit circle nearest `(3, 4)` — the sum pulling, the
1138    /// circle holding exactly, however hard the sum pulls — found quickly:
1139    /// the constraint's curvature is learned, not stumbled over. Found as
1140    /// near as the sum tells: `ε` along the circle from the nearest point
1141    /// lowers the sum, about 16, by `5 ε²`, lost in its width below
1142    /// `ε` of a few `1e-8` — where the minimizer stops is then a matter of
1143    /// where its last step happened to land.
1144    #[test]
1145    fn minimizes_on_a_circle() {
1146        let outcome = minimize(
1147            &Euclidean,
1148            |x: &Vec<Scalar>| {
1149                let (a, b) = (x[0], x[1]);
1150                Some(Evaluation {
1151                    constraints: rows(
1152                        vec![a.mul(a).add(b.mul(b)).sub(Scalar::ONE)],
1153                        vec![vec![Scalar::TWO.mul(a), Scalar::TWO.mul(b)]],
1154                    ),
1155                    sum: rows(
1156                        vec![a.sub(s(3.0)), b.sub(s(4.0))],
1157                        vec![
1158                            vec![Scalar::ONE, Scalar::ZERO],
1159                            vec![Scalar::ZERO, Scalar::ONE],
1160                        ],
1161                    ),
1162                })
1163            },
1164            vec![s(1.0), s(0.0)],
1165            options(),
1166        )
1167        .unwrap();
1168        let x = at(&outcome);
1169        assert!(
1170            (x[0] - 0.6).abs() < 1e-9 && (x[1] - 0.8).abs() < 1e-9,
1171            "{outcome:?}"
1172        );
1173        assert!(outcome.iterations < 30, "{outcome:?}");
1174    }
1175
1176    /// Two constraints that say the same are met as one.
1177    #[test]
1178    fn repeated_constraints_are_met() {
1179        let outcome = minimize(
1180            &Euclidean,
1181            |x: &Vec<Scalar>| {
1182                let c = x[0].add(x[1]).sub(Scalar::ONE);
1183                Some(Evaluation {
1184                    constraints: rows(vec![c, c], vec![vec![Scalar::ONE, Scalar::ONE]; 2]),
1185                    sum: rows(vec![x[0]], vec![vec![Scalar::ONE, Scalar::ZERO]]),
1186                })
1187            },
1188            vec![s(0.3), s(0.3)],
1189            options(),
1190        )
1191        .unwrap();
1192        let x = at(&outcome);
1193        assert!(
1194            x[0].abs() < 1e-15 && (x[1] - 1.0).abs() < 1e-15,
1195            "{outcome:?}"
1196        );
1197    }
1198
1199    /// A variable no residual depends on, between two that are minimized,
1200    /// stays put while they go where the sum wants them.
1201    #[test]
1202    fn a_free_variable_between_others_stays_put() {
1203        let outcome = minimize(
1204            &Euclidean,
1205            |x: &Vec<Scalar>| {
1206                Some(Evaluation {
1207                    constraints: Rows::default(),
1208                    sum: rows(
1209                        vec![x[0].sub(Scalar::ONE), x[2].sub(Scalar::TWO)],
1210                        vec![
1211                            vec![Scalar::ONE, Scalar::ZERO, Scalar::ZERO],
1212                            vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE],
1213                        ],
1214                    ),
1215                })
1216            },
1217            vec![s(0.0), s(0.5), s(0.0)],
1218            options(),
1219        )
1220        .unwrap();
1221        assert_eq!(at(&outcome), [1.0, 0.5, 2.0], "{outcome:?}");
1222    }
1223
1224    /// A point held a unit from a fixed one, pulled towards `(0, 3)` from
1225    /// `(1, 0)`: it turns about the fixed point until it is straight below
1226    /// the pull — a pull it can never meet, against a constraint that curves.
1227    /// The residuals of [`a_held_point_follows_a_pull_round`] at `x`.
1228    fn held_point(x: &[Scalar]) -> Option<Evaluation> {
1229        {
1230            {
1231                let (ax, ay, bx, by) = (x[0], x[1], x[2], x[3]);
1232                let (dx, dy) = (bx.sub(ax), by.sub(ay));
1233                let l = dx.mul(dx).add(dy.mul(dy)).sqrt().ok()?;
1234                let (ux, uy) = (dx.div(l).ok()?, dy.div(l).ok()?);
1235                let z = Scalar::ZERO;
1236                let w = s(1e-4);
1237                Some(Evaluation {
1238                    constraints: rows(
1239                        vec![ax, ay, l.sub(Scalar::ONE)],
1240                        vec![
1241                            vec![Scalar::ONE, z, z, z],
1242                            vec![z, Scalar::ONE, z, z],
1243                            vec![ux.neg(), uy.neg(), ux, uy],
1244                        ],
1245                    ),
1246                    sum: rows(
1247                        vec![bx, by.sub(s(3.0)), w.mul(ax), w.mul(ay)],
1248                        vec![
1249                            vec![z, z, Scalar::ONE, z],
1250                            vec![z, z, z, Scalar::ONE],
1251                            vec![w, z, z, z],
1252                            vec![z, w, z, z],
1253                        ],
1254                    ),
1255                })
1256            }
1257        }
1258    }
1259
1260    #[test]
1261    fn a_held_point_follows_a_pull_round() {
1262        let outcome = minimize(
1263            &Euclidean,
1264            |x: &Vec<Scalar>| held_point(x),
1265            vec![s(0.0), s(0.0), s(1.0), s(0.0)],
1266            Options {
1267                max_step: Scalar::ONE,
1268                ..options()
1269            },
1270        )
1271        .unwrap();
1272        let x = at(&outcome);
1273        assert!(
1274            x[2].abs() < 1e-9 && (x[3] - 1.0).abs() < 1e-9,
1275            "{outcome:?}"
1276        );
1277    }
1278
1279    /// Off a constraint, a step — however damped — corrects it: what holds
1280    /// the constraints is their linearization, which the damping does not
1281    /// weaken. The point held a unit from a fixed one (see
1282    /// [`a_held_point_follows_a_pull_round`]), where it once stalled.
1283    #[test]
1284    fn a_step_corrects_a_violated_constraint() {
1285        let (bx, by) = (s(0.0004572033706418954), s(1.0003621608700888));
1286        let l = bx.mul(bx).add(by.mul(by)).sqrt().unwrap();
1287        let (ux, uy) = (bx.div(l).unwrap(), by.div(l).unwrap());
1288        let (z, w) = (Scalar::ZERO, s(1e-4));
1289        let e = Evaluation {
1290            constraints: rows(
1291                vec![z, z, l.sub(Scalar::ONE)],
1292                vec![
1293                    vec![Scalar::ONE, z, z, z],
1294                    vec![z, Scalar::ONE, z, z],
1295                    vec![ux.neg(), uy.neg(), ux, uy],
1296                ],
1297            ),
1298            sum: rows(
1299                vec![bx, by.sub(s(3.0)), z, z],
1300                vec![
1301                    vec![z, z, Scalar::ONE, z],
1302                    vec![z, z, z, Scalar::ONE],
1303                    vec![w, z, z, z],
1304                    vec![z, w, z, z],
1305                ],
1306            ),
1307        };
1308        let b = vec![vec![Scalar::ZERO; 4]; 4];
1309        for mu in [
1310            1e-30,
1311            1e-12,
1312            4.115226337448558e-6,
1313            1e-3,
1314            1.0,
1315            1e6,
1316            1e12,
1317            1e30,
1318        ] {
1319            let Step { delta, lambda, .. } = step(&e, &b, &[true; 4], s(mu)).unwrap().unwrap();
1320            let after = e.constraints.squared(&delta).sqrt().unwrap();
1321            assert!(
1322                after.definitely_less(l.sub(Scalar::ONE)),
1323                "damped {mu}: {delta:?}, {lambda:?}, violation after {after:?}"
1324            );
1325        }
1326    }
1327
1328    /// A step meets the linearized constraints, at every damping — here at
1329    /// the held point (see [`a_held_point_follows_a_pull_round`]) nearly on
1330    /// its circle, where one did not.
1331    #[test]
1332    fn a_step_meets_the_linearized_constraints() {
1333        let x = [
1334            s(0.0),
1335            s(0.0),
1336            s(-4.223620663585878e-8),
1337            s(1.0000000001445193),
1338        ];
1339        let e = held_point(&x).unwrap();
1340        let b = vec![vec![Scalar::ZERO; 4]; 4];
1341        for mu in [4.572473708276175e-7, 1e-3, 1.0] {
1342            let Step { delta, lambda, .. } = step(&e, &b, &[true; 4], s(mu)).unwrap().unwrap();
1343            let after: Vec<Scalar> = e
1344                .constraints
1345                .values
1346                .iter()
1347                .zip(&e.constraints.jacobian)
1348                .map(|(c, row)| c.add(dot(row, &delta)))
1349                .collect();
1350            assert!(
1351                after.iter().all(|a| a.abs().to_f64() < 1e-14),
1352                "damped {mu}: {delta:?}, {lambda:?}, linearized after {after:?}"
1353            );
1354        }
1355    }
1356
1357    /// Two constraints that say the same: the step meets both, from the one
1358    /// that counts.
1359    #[test]
1360    fn a_step_meets_repeated_constraints() {
1361        let c = s(0.3).add(s(0.3)).sub(Scalar::ONE);
1362        let e = Evaluation {
1363            constraints: rows(vec![c, c], vec![vec![Scalar::ONE, Scalar::ONE]; 2]),
1364            sum: rows(vec![s(0.3)], vec![vec![Scalar::ONE, Scalar::ZERO]]),
1365        };
1366        let Step { delta, lambda, .. } =
1367            step(&e, &vec![vec![Scalar::ZERO; 2]; 2], &[true; 2], s(1e-3))
1368                .unwrap()
1369                .unwrap();
1370        let after: Vec<f64> = e
1371            .constraints
1372            .values
1373            .iter()
1374            .zip(&e.constraints.jacobian)
1375            .map(|(c, row)| c.add(dot(row, &delta)).to_f64())
1376            .collect();
1377        assert!(
1378            after.iter().all(|a| a.abs() < 1e-14),
1379            "{delta:?} {lambda:?} {after:?}"
1380        );
1381    }
1382}