Skip to main content

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}