pounce_algorithm/line_search/ls_acceptor.rs
1//! Line-search acceptor trait — port of `IpBacktrackingLSAcceptor.hpp`
2//! and `IpLineSearch.hpp`.
3
4use crate::ipopt_cq::IpoptCqHandle;
5use crate::ipopt_data::IpoptDataHandle;
6use crate::iterates_vector::IteratesVector;
7use crate::line_search::filter_acceptor::AcceptDecision;
8use crate::restoration::OrigProgressCallback;
9use pounce_common::types::Number;
10
11/// Acceptor side of the backtracking line search. Concrete impls:
12/// [`super::filter_acceptor::FilterLsAcceptor`] (Phase 7),
13/// `PenaltyLsAcceptor` (Phase 10), `CGPenaltyLsAcceptor` (Phase 10).
14///
15/// The driver calls `check_trial_point` on each backtracking step.
16/// Acceptors that need the trial-iterate components (rather than
17/// scalar `(theta, phi)`) can extend this surface in later phases —
18/// the filter acceptor only needs the four scalars upstream feeds in
19/// at line `IpFilterLSAcceptor.cpp:CheckAcceptabilityOfTrialPoint`.
20pub trait BacktrackingLsAcceptor {
21 /// Reset acceptor state for a new outer iteration.
22 fn reset(&mut self);
23
24 /// Hook called once per outer iteration, after the search direction
25 /// `delta` has been computed and before the α-loop. Mirrors
26 /// `IpPenaltyLSAcceptor.cpp:InitThisLineSearch` — the penalty
27 /// acceptor uses it to snapshot reference (θ, φ, ∇φᵀδ, δᵀWδ) and to
28 /// bump the penalty parameter ν. Default: no-op (filter acceptor
29 /// has nothing to cache between α-loop iterations).
30 fn init_this_line_search(
31 &mut self,
32 _data: &IpoptDataHandle,
33 _cq: &IpoptCqHandle,
34 _delta: &IteratesVector,
35 ) {
36 }
37
38 /// The penalty acceptor's registered constants, in the order
39 /// `(nu_init, nu_inc, rho, eta_penalty)`. `None` for acceptors that
40 /// have no penalty parameter (the filter acceptor).
41 ///
42 /// All four are registered options whose only consumer lives inside
43 /// the acceptor, and until #551 none of them had a read site. With
44 /// the acceptor reachable only as `dyn BacktrackingLsAcceptor`, the
45 /// furthest a test could follow such an option was the builder
46 /// struct — one hop short of the object that uses the value, which
47 /// is exactly the gap that let `limited_memory_initialization` look
48 /// wired while it was not (#677). This closes that hop.
49 fn penalty_parameters(&self) -> Option<(Number, Number, Number, Number)> {
50 None
51 }
52
53 /// Compute the minimum primal step length below which the
54 /// driver should declare a tiny step / hand off to restoration.
55 /// Mirrors `IpFilterLSAcceptor.cpp:CalculateAlphaMin` — the value
56 /// depends on the current `(theta, d_phi)` pair (the directional
57 /// derivative of the barrier objective along the search step) and
58 /// on the acceptor's lazily-initialised `theta_min`. Default impl
59 /// returns 0.0 so non-filter acceptors degenerate to the driver's
60 /// own absolute `alpha_min` floor.
61 fn calc_alpha_min(&mut self, _d_phi: Number, _theta: Number) -> Number {
62 0.0
63 }
64
65 /// Tell the acceptor the round-off floor of `theta` at the current
66 /// iterate (gh#945), in `theta`'s own units. Default: ignore it, which
67 /// is what every acceptor without a filter does.
68 fn set_theta_roundoff_floor(&mut self, _floor: Number) {}
69
70 /// Decide whether the trial `(theta_trial, phi_trial)` at primal
71 /// step `alpha_primal` is acceptable, given the current iterate's
72 /// `(theta, phi)` and the directional derivative `d_phi`.
73 /// Default: always accept (lets stub acceptors compose without
74 /// interfering with the driver's α-loop).
75 ///
76 /// Mutable receiver so concrete acceptors (notably
77 /// [`super::filter_acceptor::FilterLsAcceptor`]) can record per-trial
78 /// state used by the filter-reset heuristic
79 /// (`IpFilterLSAcceptor.cpp:407-433`).
80 fn check_trial_point(
81 &mut self,
82 _alpha_primal: Number,
83 _theta: Number,
84 _phi: Number,
85 _d_phi: Number,
86 _theta_trial: Number,
87 _phi_trial: Number,
88 ) -> AcceptDecision {
89 AcceptDecision::Accept
90 }
91
92 /// Post-accept hook — port of
93 /// `IpFilterLSAcceptor::UpdateForNextIteration`. Both decides the
94 /// `info_alpha_primal_char` tag *and* augments the filter when
95 /// upstream would. Returns:
96 ///
97 /// * `'f'` — F-type Armijo step (`IsFtype && ArmijoHolds`); filter
98 /// is **not** augmented.
99 /// * `'h'` — anything else (`!IsFtype || !ArmijoHolds`); filter
100 /// **is** augmented with `(theta_add, phi_add) = ((1 - γ_θ)·θ_ref,
101 /// φ_ref - γ_φ·θ_ref)`.
102 ///
103 /// The driver calls this once per accepted step, after
104 /// `check_trial_point` returns Accept and before
105 /// `accept_trial_point` promotes `trial → curr`. Default impl
106 /// returns `'h'` (no filter), so non-filter acceptors remain valid.
107 fn update_for_next_iteration(
108 &mut self,
109 _alpha_primal: Number,
110 _theta: Number,
111 _phi: Number,
112 _d_phi: Number,
113 _phi_trial: Number,
114 ) -> char {
115 'h'
116 }
117
118 /// Build the orig-progress callback the inner restoration IPM
119 /// should consult to decide whether the recovered iterate is
120 /// acceptable to *this* (outer) acceptor's filter and reference
121 /// iterate. Mirrors upstream
122 /// `IpRestoFilterConvCheck::SetOrigLSAcceptor` /
123 /// `TestOrigProgress`. Default returns `None` — penalty / CG-penalty
124 /// acceptors do not gate restoration on a filter, so they fall
125 /// through to the kappa-reduction-only guard.
126 ///
127 /// `reference_theta` and `reference_barr` are the outer iterate's
128 /// `(curr_constraint_violation, curr_barrier_obj)` at restoration
129 /// entry; `obj_max_inc` is the upstream `obj_max_inc` option
130 /// (default 5.0).
131 fn make_orig_progress_check(
132 &self,
133 _reference_theta: Number,
134 _reference_barr: Number,
135 _obj_max_inc: Number,
136 ) -> Option<OrigProgressCallback> {
137 None
138 }
139
140 /// Hook called by the algorithm immediately before invoking the
141 /// restoration phase — port of
142 /// `IpFilterLSAcceptor::PrepareRestoPhaseStart` →
143 /// `AugmentFilter` (`IpFilterLSAcceptor.cpp:898-901`, called from
144 /// `IpBacktrackingLineSearch.cpp:566`). The filter acceptor
145 /// augments the filter with the resto-entry iterate's shrunk
146 /// envelope `((1 - γ_θ)·θ_ref, φ_ref - γ_φ·θ_ref)`. After
147 /// restoration recovers, the outer's Newton step is then forced
148 /// by the filter to make real progress vs the entry point —
149 /// without this, the outer can accept null-progress 'h' steps
150 /// and re-enter restoration (observed on DECONVBNE: 323 R-accepts
151 /// vs ipopt's 21). Default: no-op for non-filter acceptors.
152 fn prepare_resto_phase_start(&mut self, _reference_theta: Number, _reference_barr: Number) {}
153
154 /// Override the filter acceptor's `theta_max_fact` (default 1e4).
155 /// Used by the resto sub-IPM wiring to bump the gate to 1e8, which
156 /// mirrors upstream `IpRestoMinC_1Nrm.cpp:91`
157 /// (`resto.theta_max_fact = 1e8`). Without this override the inner
158 /// IPM's first line search caps `theta_max = 1e4` (since reference
159 /// θ ≈ 0 after slack init), and the first non-trivial trial whose
160 /// resto-NLP θ_trial exceeds 1e4 is rejected at the `theta_max`
161 /// gate before reaching f-type/Armijo dispatch. Default: no-op for
162 /// non-filter acceptors.
163 fn set_theta_max_fact(&mut self, _theta_max_fact: Number) {}
164
165 /// Tell the acceptor how many constraint rows back `theta`'s 1-norm
166 /// (`dim(c) + dim(d - s)`). The filter acceptor floors its
167 /// `theta_max` reference at `theta_max_row_scale_kappa * rows` so
168 /// the ceiling does not degenerate to the bare constant `1e4` on a
169 /// large-`m` model with a feasible starting point. Called once per
170 /// solve, before the first `theta_max` lock. Default: no-op.
171 fn set_theta_rows(&mut self, _rows: Number) {}
172
173 /// Override the filter acceptor's `theta_max_row_scale_kappa`.
174 /// Used by the resto sub-IPM wiring to set it to `0`, i.e. to opt
175 /// the inner IPM out of the row-count floor: upstream already
176 /// covers the resto phase's version of the same degeneracy with
177 /// its hard-coded `theta_max_fact = 1e8`
178 /// (`IpRestoMinC_1Nrm.cpp:91`), and stacking the row floor on top
179 /// of that would push the inner ceiling to `1e8 · m` — effectively
180 /// removing it. Default: no-op for non-filter acceptors.
181 fn set_theta_max_row_scale_kappa(&mut self, _kappa: Number) {}
182
183 /// Override the filter acceptor's `theta_max_adaptive_trigger`.
184 /// Used by the resto sub-IPM wiring to set it to `0` for the same
185 /// reason as [`Self::set_theta_max_row_scale_kappa`]: the inner IPM
186 /// already runs at `theta_max_fact = 1e8`, which is upstream's own
187 /// hard-coded version of this rescue, so letting the adaptive rule
188 /// ratchet on top of that would raise an already-enormous ceiling
189 /// further. Default: no-op for non-filter acceptors.
190 fn set_theta_max_adaptive_trigger(&mut self, _trigger: u32) {}
191}