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 /// Compute the minimum primal step length below which the
39 /// driver should declare a tiny step / hand off to restoration.
40 /// Mirrors `IpFilterLSAcceptor.cpp:CalculateAlphaMin` — the value
41 /// depends on the current `(theta, d_phi)` pair (the directional
42 /// derivative of the barrier objective along the search step) and
43 /// on the acceptor's lazily-initialised `theta_min`. Default impl
44 /// returns 0.0 so non-filter acceptors degenerate to the driver's
45 /// own absolute `alpha_min` floor.
46 fn calc_alpha_min(&mut self, _d_phi: Number, _theta: Number) -> Number {
47 0.0
48 }
49
50 /// Decide whether the trial `(theta_trial, phi_trial)` at primal
51 /// step `alpha_primal` is acceptable, given the current iterate's
52 /// `(theta, phi)` and the directional derivative `d_phi`.
53 /// Default: always accept (lets stub acceptors compose without
54 /// interfering with the driver's α-loop).
55 ///
56 /// Mutable receiver so concrete acceptors (notably
57 /// [`super::filter_acceptor::FilterLsAcceptor`]) can record per-trial
58 /// state used by the filter-reset heuristic
59 /// (`IpFilterLSAcceptor.cpp:407-433`).
60 fn check_trial_point(
61 &mut self,
62 _alpha_primal: Number,
63 _theta: Number,
64 _phi: Number,
65 _d_phi: Number,
66 _theta_trial: Number,
67 _phi_trial: Number,
68 ) -> AcceptDecision {
69 AcceptDecision::Accept
70 }
71
72 /// Post-accept hook — port of
73 /// `IpFilterLSAcceptor::UpdateForNextIteration`. Both decides the
74 /// `info_alpha_primal_char` tag *and* augments the filter when
75 /// upstream would. Returns:
76 ///
77 /// * `'f'` — F-type Armijo step (`IsFtype && ArmijoHolds`); filter
78 /// is **not** augmented.
79 /// * `'h'` — anything else (`!IsFtype || !ArmijoHolds`); filter
80 /// **is** augmented with `(theta_add, phi_add) = ((1 - γ_θ)·θ_ref,
81 /// φ_ref - γ_φ·θ_ref)`.
82 ///
83 /// The driver calls this once per accepted step, after
84 /// `check_trial_point` returns Accept and before
85 /// `accept_trial_point` promotes `trial → curr`. Default impl
86 /// returns `'h'` (no filter), so non-filter acceptors remain valid.
87 fn update_for_next_iteration(
88 &mut self,
89 _alpha_primal: Number,
90 _theta: Number,
91 _phi: Number,
92 _d_phi: Number,
93 _phi_trial: Number,
94 ) -> char {
95 'h'
96 }
97
98 /// Build the orig-progress callback the inner restoration IPM
99 /// should consult to decide whether the recovered iterate is
100 /// acceptable to *this* (outer) acceptor's filter and reference
101 /// iterate. Mirrors upstream
102 /// `IpRestoFilterConvCheck::SetOrigLSAcceptor` /
103 /// `TestOrigProgress`. Default returns `None` — penalty / CG-penalty
104 /// acceptors do not gate restoration on a filter, so they fall
105 /// through to the kappa-reduction-only guard.
106 ///
107 /// `reference_theta` and `reference_barr` are the outer iterate's
108 /// `(curr_constraint_violation, curr_barrier_obj)` at restoration
109 /// entry; `obj_max_inc` is the upstream `obj_max_inc` option
110 /// (default 5.0).
111 fn make_orig_progress_check(
112 &self,
113 _reference_theta: Number,
114 _reference_barr: Number,
115 _obj_max_inc: Number,
116 ) -> Option<OrigProgressCallback> {
117 None
118 }
119
120 /// Hook called by the algorithm immediately before invoking the
121 /// restoration phase — port of
122 /// `IpFilterLSAcceptor::PrepareRestoPhaseStart` →
123 /// `AugmentFilter` (`IpFilterLSAcceptor.cpp:898-901`, called from
124 /// `IpBacktrackingLineSearch.cpp:566`). The filter acceptor
125 /// augments the filter with the resto-entry iterate's shrunk
126 /// envelope `((1 - γ_θ)·θ_ref, φ_ref - γ_φ·θ_ref)`. After
127 /// restoration recovers, the outer's Newton step is then forced
128 /// by the filter to make real progress vs the entry point —
129 /// without this, the outer can accept null-progress 'h' steps
130 /// and re-enter restoration (observed on DECONVBNE: 323 R-accepts
131 /// vs ipopt's 21). Default: no-op for non-filter acceptors.
132 fn prepare_resto_phase_start(&mut self, _reference_theta: Number, _reference_barr: Number) {}
133
134 /// Override the filter acceptor's `theta_max_fact` (default 1e4).
135 /// Used by the resto sub-IPM wiring to bump the gate to 1e8, which
136 /// mirrors upstream `IpRestoMinC_1Nrm.cpp:91`
137 /// (`resto.theta_max_fact = 1e8`). Without this override the inner
138 /// IPM's first line search caps `theta_max = 1e4` (since reference
139 /// θ ≈ 0 after slack init), and the first non-trivial trial whose
140 /// resto-NLP θ_trial exceeds 1e4 is rejected at the `theta_max`
141 /// gate before reaching f-type/Armijo dispatch. Default: no-op for
142 /// non-filter acceptors.
143 fn set_theta_max_fact(&mut self, _theta_max_fact: Number) {}
144}