pounce_algorithm/restoration.rs
1//! `RestorationPhase` trait — port of `IpRestoPhase.hpp`.
2//!
3//! Defined here in `pounce-algorithm` (rather than `pounce-restoration`)
4//! so that [`crate::ipopt_alg::IpoptAlgorithm`] can call into it without
5//! creating a circular crate dependency. Concrete impls (the default
6//! `MinC1NormRestoration`, the rare `RestoRestorationPhase`) live in
7//! `pounce-restoration` and `impl RestorationPhase for ...`.
8//!
9//! Called by the main loop when the line search exhausts its alpha
10//! reductions without acceptance (or by the iterate initializer when
11//! `start_with_resto = true`). On success the impl writes a recovered
12//! iterate to `data.trial` and the main loop accepts it; on failure the
13//! main loop surfaces `SolverReturn::RestorationFailure`.
14
15use crate::ipopt_cq::IpoptCqHandle;
16use crate::ipopt_data::IpoptDataHandle;
17use crate::ipopt_nlp::IpoptNlp;
18use crate::kkt::aug_system_solver::AugSystemSolver;
19use pounce_common::types::Number;
20use std::cell::RefCell;
21use std::rc::Rc;
22
23/// Callback that the inner restoration IPM consults at every iteration
24/// to decide whether the recovered iterate is acceptable to the *outer*
25/// algorithm's filter and reference iterate. Mirrors upstream
26/// `IpRestoFilterConvCheck::TestOrigProgress`
27/// (`IpRestoFilterConvCheck.cpp:53-80`): given `(orig_trial_barr,
28/// orig_trial_theta)` evaluated at the inner iterate's `(x_orig, s)`
29/// slice, returns `true` iff
30///
31/// 1. the pair is acceptable to the outer filter, AND
32/// 2. the pair is acceptable to the outer reference iterate (with the
33/// rapid-barrier-increase guard disabled — `force_armijo=true` /
34/// `called_from_restoration=true`).
35///
36/// Constructed by [`crate::line_search::ls_acceptor::BacktrackingLsAcceptor::make_orig_progress_check`]
37/// at restoration entry, with the outer filter cloned and the outer
38/// reference `(theta, barr)` snapshotted in the closure.
39pub type OrigProgressCallback = Box<dyn Fn(Number, Number) -> bool>;
40
41/// Restoration's release test as the restoration phase consumes it: given a
42/// restoration iterate's `(x, s)` in the **outer** problem's space, may the
43/// outer phase resume from it?
44///
45/// Built by the outer algorithm, which owns the quantities the outer filter
46/// holds. It evaluates the outer problem's own barrier objective and 1-norm
47/// constraint violation at `(x, s)` (`trial_barrier_obj`,
48/// `trial_constraint_violation`, at the outer `mu`), exactly as upstream's
49/// `IpRestoFilterConvCheck::TestOrigProgress` does by setting the original
50/// problem's trial point, and hands that pair to the
51/// [`OrigProgressCallback`]. The restoration crate cannot compute the pair
52/// itself without re-deriving the barrier terms, damping included, that the
53/// filter's entries were made with; it used to pass the plain objective and
54/// the max-norm violation instead, which the filter does not hold
55/// (gh#1042).
56pub type OrigProgressCheck =
57 Box<dyn Fn(&dyn pounce_linalg::Vector, &dyn pounce_linalg::Vector) -> bool>;
58
59/// Outcome of a restoration attempt. Mirrors upstream's `bool` return
60/// from `RestorationPhase::PerformRestoration` plus the in-band
61/// `info_skip_output` / `iter_count` side-effects that the impl writes
62/// to `data` directly.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum RestorationOutcome {
65 /// Resto succeeded; outer loop should `accept_trial_point` and
66 /// continue. The impl has written the recovered iterate into
67 /// `data.trial`, set `info_skip_output = true`, and updated the
68 /// info counters.
69 Recovered,
70 /// Resto failed. Outer loop maps this to
71 /// `SolverReturn::RestorationFailure`.
72 Failed,
73 /// The inner sub-IPM converged its KKT system but the orig-NLP
74 /// constraint violation at the converged point is still well above
75 /// `tol`. Mirrors the `LOCALLY_INFEASIBLE` exception thrown from
76 /// `IpRestoConvCheck.cpp:240`. Outer loop maps this to
77 /// `SolverReturn::LocalInfeasibility`.
78 LocallyInfeasible,
79 /// The user's intermediate callback returned `false` from a
80 /// restoration-phase fire (gh#645). Outer loop maps this to
81 /// `SolverReturn::UserRequestedStop` **without** promoting the
82 /// staged trial point, so the solve hands back the last iterate
83 /// accepted for the *original* NLP rather than a point of the
84 /// restoration subproblem — the same discipline the pounce#244
85 /// deadline exit already follows.
86 UserRequestedStop,
87 /// The original NLP is square and the restoration phase reached a
88 /// point feasible for it to `constr_viol_tol`. Port of the
89 /// `FEASIBILITY_PROBLEM_SOLVED` throw at `IpRestoMinC_1Nrm.cpp:269`;
90 /// the outer loop maps this to `SolverReturn::FeasiblePointFound`
91 /// (`IpIpoptAlg.cpp:542`) after recomputing the multipliers of the
92 /// feasibility problem. The impl has already promoted the recovered
93 /// point to `data.curr`.
94 FeasiblePointFound,
95}
96
97pub trait RestorationPhase {
98 /// Inner-IPM iteration count from the most recent
99 /// `perform_restoration` call. Read by `IpoptAlgorithm` for the
100 /// pounce#12 audit counters in `SolveStatistics`. Default 0; the
101 /// concrete `MinC1NormRestoration` impl stashes
102 /// `RestoSolveResult::iter_count` and returns it here.
103 fn last_inner_iter_count(&self) -> pounce_common::types::Index {
104 0
105 }
106
107 /// Whether the most recent `perform_restoration` call returned
108 /// [`RestorationOutcome::Recovered`] with a point its own convergence
109 /// check never released (gh#1042): the restoration sub-problem
110 /// converged at a point feasible for the original NLP, but that point
111 /// either failed the outer filter / reference-iterate test or did not
112 /// cut the violation by `kappa_resto`. Upstream fails restoration there
113 /// (`RESTORATION_CONVERGED_TO_FEASIBLE_POINT`,
114 /// `IpRestoConvCheck.cpp:227-234`); pounce hands the point back, and
115 /// the outer loop uses this to tell when that hand-back did not pay
116 /// off. Default `false`.
117 fn last_recovery_unreleased(&self) -> bool {
118 false
119 }
120
121 /// Drive a feasibility-restoration sub-solve. The impl reads the
122 /// outer iterate from `data.curr`, the original NLP from `nlp`,
123 /// uses `aug_solver` for any post-success multiplier-recomputation
124 /// least-square solve, and on success writes the recovered iterate
125 /// into `data.trial`. Default returns
126 /// [`RestorationOutcome::Failed`] — the trait surface is uniform
127 /// for `AlgBuilder` even when no concrete restoration is wired.
128 fn perform_restoration(
129 &mut self,
130 _data: &IpoptDataHandle,
131 _cq: &IpoptCqHandle,
132 _nlp: &Rc<RefCell<dyn IpoptNlp>>,
133 _aug_solver: &mut dyn AugSystemSolver,
134 ) -> RestorationOutcome {
135 RestorationOutcome::Failed
136 }
137
138 /// Forward the outer interactive debugger onto the restoration inner
139 /// IPM so the same debugger can step the sub-solve. Default no-op.
140 fn set_debug_hook(
141 &mut self,
142 _hook: Option<std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>>,
143 ) {
144 }
145
146 /// Forward the user's TNLP onto the restoration inner IPM so its
147 /// `intermediate_callback` fires from the sub-solve too (gh#645).
148 /// Default no-op, like [`Self::set_debug_hook`] above, which this
149 /// deliberately mirrors — the debugger took the same route first.
150 ///
151 /// Without this the callback fires only from the outer loop, so a
152 /// caller is blind for the whole of restoration. That is the phase
153 /// most likely to overrun a control period, which makes it the
154 /// phase a real-time caller most needs to be able to abort in.
155 ///
156 /// The inner IPM iterates on the min-C1-norm feasibility
157 /// subproblem, so the stats those fires carry describe *that*
158 /// problem, not the user's NLP. `AlgorithmMode::RestorationPhaseMode`
159 /// on each such fire is what makes them interpretable, and
160 /// [`crate::ipopt_alg::IpoptAlgorithm::fires_as_restoration`] is
161 /// what sets it — see the note there on why the live-inspector
162 /// context is deliberately *not* installed for these fires.
163 fn set_intermediate_tnlp(
164 &mut self,
165 _tnlp: Option<std::rc::Rc<std::cell::RefCell<dyn pounce_nlp::tnlp::TNLP>>>,
166 ) {
167 }
168
169 /// Inject the orig-progress callback the inner IPM should consult at
170 /// every iteration. Mirrors upstream
171 /// `IpRestoFilterConvCheck::SetOrigLSAcceptor` (the outer line
172 /// search hands its acceptor to the resto conv check at restoration
173 /// entry). Default no-op so non-filter-aware drivers compose.
174 fn set_orig_progress_check(&mut self, _cb: Option<OrigProgressCheck>) {}
175
176 /// Propagate the outer algorithm's per-iteration print gate to the
177 /// restoration driver so the nested restoration IPM honors
178 /// `print_level == 0` instead of leaking its `r`-suffixed iteration
179 /// table to stdout. Default no-op for drivers without a nested IPM.
180 fn set_print_iter_output(&mut self, _enabled: bool) {}
181}