spectrafit_trust_region/report.rs
1//! Generic trust-region outcome types, shared by every method built on this
2//! framework (Levenberg–Marquardt, dogleg, Newton-CG/Steihaug, …).
3//!
4//! A method's driver constructs a [`Report`] with a [`Termination`] reason; the
5//! framework owns these so consumers speak one vocabulary regardless of which
6//! method ran.
7
8/// Why the solve stopped.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Termination {
11 /// `‖Jᵀr‖_∞ ≤ gtol` — first-order optimality.
12 Gtol,
13 /// Relative cost decrease `≤ ftol`.
14 Ftol,
15 /// Relative step size `≤ xtol`.
16 Xtol,
17 /// Residuals are (numerically) zero.
18 ResidualsZero,
19 /// Exhausted the residual-evaluation budget.
20 MaxEval,
21 /// The trust-region control diverged without an accepted step.
22 NoImprovement,
23 /// A residual/Jacobian evaluation failed.
24 NumericalError,
25}
26
27impl Termination {
28 /// Whether the run reached a convergence criterion (vs. a budget/error stop).
29 pub fn was_successful(&self) -> bool {
30 matches!(
31 self,
32 Termination::Gtol | Termination::Ftol | Termination::Xtol | Termination::ResidualsZero
33 )
34 }
35}
36
37/// Outcome of a solve. The optimised parameters live in the `problem` (read via
38/// [`TrustRegionProblem::params`](crate::TrustRegionProblem::params)); this only
39/// carries diagnostics.
40///
41/// Not `Copy`: it carries the per-iteration convergence trajectory (`cost_history`
42/// / `gradient_norm_history`) as owned `Vec`s. These are observability only — they
43/// do not affect the optimisation and are recorded once per accepted point plus the
44/// terminal point.
45#[derive(Debug, Clone)]
46pub struct Report {
47 /// Why the loop stopped.
48 pub termination: Termination,
49 /// Accepted iterations.
50 pub n_iter: usize,
51 /// Residual evaluations.
52 pub n_residual_evals: usize,
53 /// Jacobian evaluations.
54 pub n_jacobian_evals: usize,
55 /// Final `½‖r‖²`.
56 pub cost: f64,
57 /// `‖Jᵀr‖_∞` **at the most recent point where the Jacobian was evaluated**
58 /// — the start of the final outer iteration.
59 ///
60 /// This is the definition for every termination reason, and it is what
61 /// makes the number comparable across them. It is deliberately *not*
62 /// "‖Jᵀr‖_∞ at the returned parameters": on an `Ftol`/`Xtol`/`MaxEval` stop
63 /// the loop exits after accepting a step, and no Jacobian has been
64 /// evaluated at that new point, so reporting a gradient there would mean
65 /// either paying for an extra Jacobian evaluation or pairing the stale `J`
66 /// with the new `r` — a product that is the gradient at neither point. The
67 /// drivers therefore report the last genuinely-evaluated gradient instead.
68 /// On a `Gtol` stop the two coincide, since that test fires before any
69 /// step is taken.
70 pub gradient_norm: f64,
71 /// `½‖r‖²` at each accepted point (index 0 = initial), ending at the terminal
72 /// cost. Empty only for an immediate pre-iteration numerical failure.
73 pub cost_history: Vec<f64>,
74 /// `‖Jᵀr‖_∞` recorded alongside each `cost_history` entry, under the same
75 /// "at the most recent Jacobian evaluation" definition as
76 /// [`gradient_norm`](Self::gradient_norm) — so the terminal entry repeats
77 /// the previous iteration's gradient when the loop stopped after an
78 /// accepted step.
79 pub gradient_norm_history: Vec<f64>,
80 /// The free-parameter vector `θ` at each accepted point, recorded alongside
81 /// each `cost_history` entry (same length and ordering). This is the raw
82 /// material for the convergence-to-truth metric `dₖ = ‖(θₖ − θ_true)/s‖₂`
83 /// on synthetic cases — observability only, it does not affect the solve.
84 /// Empty for solvers that do not track it (only the faer LM driver records
85 /// it today; the trust-region / dogleg / newton-cg drivers leave it empty).
86 pub params_history: Vec<Vec<f64>>,
87}