Skip to main content

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}