Skip to main content

pounce_algorithm/conv_check/
trait.rs

1//! `ConvCheck` trait — port of `IpConvCheck.hpp`.
2//!
3//! Upstream `ConvergenceCheck::CheckConvergence` reads the NLP error
4//! and iter count off `IpData()`/`IpCq()`. The default trait method
5//! pushes that read to the caller (the main loop in `ipopt_alg.rs`)
6//! so simple convergence policies stay pure scalar state machines
7//! over `(nlp_err, iter_count)`. The richer
8//! [`ConvCheck::check_convergence_with_state`] entry point exposes
9//! the live `(IpoptData, IpoptCq)` so policies that need iterate
10//! components — notably the restoration-side
11//! `RestoFilterConvergenceCheck::TestOrigProgress` — can read them.
12//! Default impl just delegates to the scalar method, preserving
13//! backwards compatibility for every existing impl.
14
15use crate::ipopt_cq::IpoptCqHandle;
16use crate::ipopt_data::IpoptDataHandle;
17use pounce_common::types::{Index, Number};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ConvergenceStatus {
21    Continue,
22    Converged,
23    /// Converged to the looser `acceptable_*` tolerance band rather
24    /// than the tight `tol` — upstream `CONVERGED_TO_ACCEPTABLE_POINT`.
25    /// Maps to `SolverReturn::StopAtAcceptablePoint` →
26    /// `ApplicationReturnStatus::SolvedToAcceptableLevel`.
27    ConvergedToAcceptable,
28    MaxIterExceeded,
29    /// `max_cpu_time` budget reached. Maps to
30    /// `SolverReturn::CpuTimeExceeded` → `MaximumCpuTimeExceeded`.
31    CpuTimeExceeded,
32    /// `max_wall_time` budget reached. Maps to
33    /// `SolverReturn::WallTimeExceeded` → `MaximumWallTimeExceeded`.
34    WallTimeExceeded,
35    /// Rapid infeasibility detection fired — the iterate is
36    /// converging to a stationary point of the constraint violation
37    /// with the violation bounded away from zero. Maps to
38    /// `SolverReturn::LocalInfeasibility`.
39    LocallyInfeasible,
40    Failed,
41}
42
43pub trait ConvCheck {
44    fn check_convergence(&mut self, nlp_err: Number, iter_count: Index) -> ConvergenceStatus;
45
46    /// State-aware convergence check. The main loop calls this on
47    /// every iteration so policies that need access to the iterate
48    /// (e.g. `RestoConvCheckAdapter`'s orig-NLP `inf_pr` evaluation
49    /// for the kappa-reduction early-exit) can read `data.curr` and
50    /// the cq layer. Default impl delegates to
51    /// [`Self::check_convergence`], so scalar-only policies don't
52    /// need to override.
53    fn check_convergence_with_state(
54        &mut self,
55        nlp_err: Number,
56        iter_count: Index,
57        _data: &IpoptDataHandle,
58        _cq: &IpoptCqHandle,
59    ) -> ConvergenceStatus {
60        self.check_convergence(nlp_err, iter_count)
61    }
62
63    /// Whether the supplied `nlp_err` is at or below the acceptable
64    /// tolerance — port of upstream
65    /// `OptimalityErrorConvergenceCheck::CurrentIsAcceptable`. Used by
66    /// the main loop to gate `StoreAcceptablePoint` /
67    /// `RestoreAcceptablePoint`. Default returns `false` so policies
68    /// that don't track an acceptable level (e.g. resto-of-resto inner
69    /// adapters) silently skip the rollback machinery.
70    fn current_is_acceptable(&self, _nlp_err: Number) -> bool {
71        false
72    }
73
74    /// State-aware acceptance check. Mirrors upstream
75    /// `OptimalityErrorConvergenceCheck::CurrentIsAcceptable` which
76    /// reads the per-component residuals and current `f` to gate the
77    /// `acceptable_dual_inf_tol` / `acceptable_constr_viol_tol` /
78    /// `acceptable_compl_inf_tol` / `acceptable_obj_change_tol`
79    /// triplet. Default delegates to the scalar [`Self::current_is_acceptable`].
80    fn current_is_acceptable_with_state(
81        &self,
82        nlp_err: Number,
83        _data: &IpoptDataHandle,
84        _cq: &IpoptCqHandle,
85    ) -> bool {
86        self.current_is_acceptable(nlp_err)
87    }
88
89    /// Record the current objective at the iterate the main loop just
90    /// stashed as the latest "acceptable point" — mirrors upstream
91    /// `OptimalityErrorConvergenceCheck::SetCurrAcceptableF`. The
92    /// recorded value feeds the `acceptable_obj_change_tol` stability
93    /// cross-check on subsequent iterates. Default no-op for policies
94    /// that don't track acceptable points.
95    fn set_curr_acceptable_obj(&mut self, _obj: Number) {}
96
97    /// Outer NLP convergence tolerance, as used by the main loop's
98    /// almost-feasible bypass guard (port of
99    /// `IpBacktrackingLineSearch.cpp:580`). Default `1e-8` matches
100    /// upstream's default `tol`.
101    fn tol_or_default(&self) -> Number {
102        1e-8
103    }
104
105    /// Live-update a named convergence tolerance mid-solve, for the
106    /// debugger's in-place option hot-swap. Returns `true` if `name`
107    /// matched a tolerance this policy owns (so the caller can report
108    /// whether it took). Default: this policy exposes no live
109    /// tolerances → `false`.
110    fn set_tolerance(&mut self, _name: &str, _value: Number) -> bool {
111        false
112    }
113}