pounce_algorithm/ipopt_data.rs
1//! Mutable algorithm state — port of `Algorithm/IpIpoptData.{hpp,cpp}`.
2//!
3//! Holds the iterate trio (`curr` / `trial` / `delta`), the affine
4//! step `delta_aff`, current barrier parameter `mu`, fraction-to-the-
5//! boundary `tau`, iteration count, tolerances, and the four PD
6//! perturbations. No additional-data plug in v1.0 (CG penalty is
7//! deferred to Phase 10).
8//!
9//! Phase 5 ships the full state holder; concrete strategies (line
10//! search, mu update, etc.) read/write fields here as their inputs
11//! and outputs.
12
13use crate::iterates_vector::IteratesVector;
14use pounce_common::timing::TimingStatistics;
15use pounce_common::types::{Index, Number};
16use pounce_linalg::SymMatrix;
17use std::cell::RefCell;
18use std::rc::Rc;
19
20/// Primal-dual perturbation triple (`delta_x`, `delta_s`, `delta_c`,
21/// `delta_d`) — port of the four `current_perturbation` fields in
22/// `IpIpoptData.hpp`.
23#[derive(Debug, Default, Clone, Copy)]
24pub struct PdPerturbations {
25 pub delta_x: Number,
26 pub delta_s: Number,
27 pub delta_c: Number,
28 pub delta_d: Number,
29}
30
31/// KKT-factorization diagnostics captured for the interactive debugger
32/// after a search-direction solve. Only populated when a debugger is
33/// installed (see `IpoptAlgorithm`); inspected via `DebugCtx::kkt`.
34#[derive(Clone, Debug, Default)]
35pub struct KktDebug {
36 /// The outer iteration this factorization was assembled at. Lets the
37 /// debugger label `viz kkt` / `viz L` with the iteration the system
38 /// actually came from — at an `iter_start` pause that's the *previous*
39 /// iteration (the step that produced the current point), not the
40 /// iterate you're standing on.
41 pub iter: i32,
42 /// Dimension of the augmented system (n + m).
43 pub dim: i32,
44 /// Negative eigenvalues reported by the factorization (-1 if the
45 /// backend doesn't provide inertia).
46 pub n_neg: i32,
47 /// Whether the backend reports inertia at all.
48 pub provides_inertia: bool,
49 /// Debug string of the last factorization status.
50 pub status: String,
51 /// Assembled KKT triplets `(dim, irn, jcn, vals)`, 1-based lower
52 /// triangle — for `viz kkt`. Captured when a debugger is attached.
53 pub matrix: Option<(i32, Vec<i32>, Vec<i32>, Vec<f64>)>,
54 /// `LDLᵀ` factor pattern (+ values) — for `viz L`. Captured only
55 /// after the debugger opts in (it's the expensive piece).
56 pub l_factor: Option<pounce_linsol::FactorPattern>,
57}
58
59/// Mutable state passed down through the algorithm. Owned by
60/// `IpoptAlgorithm`; strategies access via `Rc<RefCell<IpoptData>>`.
61pub struct IpoptData {
62 pub curr: Option<IteratesVector>,
63 pub trial: Option<IteratesVector>,
64 pub delta: Option<IteratesVector>,
65 pub delta_aff: Option<IteratesVector>,
66 /// Pure centering step — solution of the primal-dual system with
67 /// RHS `(0, 0, 0, 0, μ̄·1, μ̄·1, μ̄·1, μ̄·1)` (where μ̄ = avrg_compl)
68 /// per upstream `IpQualityFunctionMuOracle.cpp:227-247`. Used by
69 /// the quality-function oracle to assemble σ-step trial points
70 /// without re-factorising for each candidate σ.
71 pub delta_cen: Option<IteratesVector>,
72
73 /// Hessian of the Lagrangian for the *current* iterate. Set by
74 /// `HessianUpdater` (exact or quasi-Newton). Mirrors `IpIpoptData::W_`.
75 pub w: Option<Rc<dyn SymMatrix>>,
76
77 pub iter_count: Index,
78 pub curr_mu: Number,
79 pub curr_tau: Number,
80 pub tol: Number,
81
82 pub perturbations: PdPerturbations,
83
84 /// KKT-factorization diagnostics for the debugger (set after a
85 /// search-direction solve when a debugger is installed). The full
86 /// matrix triplets and `LDLᵀ` factor are captured here whenever the
87 /// debugger is stepping (see `DebugHook::wants_kkt_capture`) and
88 /// dropped when it detaches, so `viz kkt` / `viz L` always have the
89 /// previous iteration's system to look back at without paying the
90 /// O(nnz) assembly during a free run.
91 pub kkt_debug: Option<KktDebug>,
92
93 /// Set after a successful trial-acceptance step in the line
94 /// search. Cleared on accept.
95 pub info_alpha_primal: Number,
96 pub info_alpha_dual: Number,
97
98 /// Mirrors `IpIpoptData::info_regu_x_`.
99 pub info_regu_x: Number,
100
101 /// Mirrors `IpIpoptData::info_skip_output_`.
102 pub info_skip_output: bool,
103
104 /// Mirrors `IpIpoptData::info_string_`. Free-form text the
105 /// iteration output appends to its line.
106 pub info_string: String,
107
108 /// Mirrors `IpIpoptData::tiny_step_flag_`. Set by the line search
109 /// when an alpha→0 trial is detected; the main loop reads it on
110 /// the next pass to decide between "tiny step accept" and bail.
111 pub tiny_step_flag: bool,
112
113 /// Emergency restoration request from the μ-update layer. Set by
114 /// [`AdaptiveMuUpdate`] when the probing oracle's input iterate
115 /// is corrupted (`curr_avrg_compl` ≫ `curr_mu`) so the main loop
116 /// invokes restoration instead of letting the oracle snap μ up
117 /// many orders of magnitude. Pounce-specific guard; no upstream
118 /// counterpart. See pounce#58.
119 pub request_resto: bool,
120
121 /// One-char marker the iteration output puts in front of
122 /// `alpha_primal` (e.g. `'f'` for filter, `'r'` for restoration,
123 /// `'h'` for the very first iterate). Mirrors
124 /// `IpIpoptData::info_alpha_primal_char_`.
125 pub info_alpha_primal_char: char,
126
127 /// Number of trial points evaluated in the most recent line
128 /// search. Mirrors `IpIpoptData::info_ls_count_`.
129 pub info_ls_count: Index,
130
131 /// The wall-clock at the last `OrigIterationOutput::WriteOutput`
132 /// pass. Phase 7 uses this to decide whether to re-print the
133 /// header. Mirrors `IpIpoptData::info_last_output_`.
134 pub info_last_output: Number,
135
136 /// Iterations since the iteration header was last printed. Phase
137 /// 7 reprints every `print_frequency_iter` lines. Mirrors
138 /// `IpIpoptData::info_iters_since_header_`.
139 pub info_iters_since_header: Index,
140
141 /// Shared per-subsystem timing accumulator. Mirrors upstream's
142 /// `IpoptData::TimingStats_`. `IpoptApplication` constructs a single
143 /// instance per solve and shares it (via `Rc`) with the algorithm,
144 /// NLP, and KKT solver so each can record its own contribution.
145 /// Defaults to a fresh empty instance for the structural unit tests
146 /// that don't go through `IpoptApplication`.
147 pub timing: Rc<TimingStatistics>,
148}
149
150impl Default for IpoptData {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl IpoptData {
157 pub fn new() -> Self {
158 Self {
159 curr: None,
160 trial: None,
161 delta: None,
162 delta_aff: None,
163 delta_cen: None,
164 w: None,
165 iter_count: 0,
166 curr_mu: 0.1,
167 curr_tau: 0.99,
168 tol: 1e-8,
169 perturbations: PdPerturbations::default(),
170 kkt_debug: None,
171 info_alpha_primal: 0.0,
172 info_alpha_dual: 0.0,
173 info_regu_x: 0.0,
174 info_skip_output: false,
175 info_string: String::new(),
176 tiny_step_flag: false,
177 request_resto: false,
178 info_alpha_primal_char: ' ',
179 info_ls_count: 0,
180 info_last_output: -1.0,
181 info_iters_since_header: 0,
182 timing: Rc::new(TimingStatistics::new()),
183 }
184 }
185
186 /// Append text to `info_string`. Mirrors `IpIpoptData::Append_info_string`.
187 pub fn append_info_string(&mut self, s: &str) {
188 self.info_string.push_str(s);
189 }
190
191 /// Reset per-iteration info fields. Mirrors the top of
192 /// `IpoptAlgorithm::Optimize`'s loop body.
193 pub fn reset_info(&mut self) {
194 self.info_string.clear();
195 self.info_skip_output = false;
196 self.info_alpha_primal_char = ' ';
197 self.info_ls_count = 0;
198 self.info_regu_x = 0.0;
199 }
200
201 /// Replace `curr` with the previously-set `trial`. Mirrors
202 /// `IpIpoptData::AcceptTrialPoint`.
203 pub fn accept_trial_point(&mut self) {
204 self.curr = self.trial.take();
205 }
206
207 /// Set the trial iterate from a primal step `delta_x`/`delta_s`
208 /// scaled by `alpha_p` and a dual step scaled by `alpha_d`.
209 /// Phase 5 ships only the structural plumbing; the actual
210 /// arithmetic is implemented once the line search lands in
211 /// Phase 7.
212 pub fn set_trial(&mut self, trial: IteratesVector) {
213 self.trial = Some(trial);
214 }
215
216 pub fn set_curr(&mut self, curr: IteratesVector) {
217 self.curr = Some(curr);
218 }
219
220 pub fn set_delta(&mut self, d: IteratesVector) {
221 self.delta = Some(d);
222 }
223
224 pub fn set_delta_aff(&mut self, d: IteratesVector) {
225 self.delta_aff = Some(d);
226 }
227
228 pub fn set_delta_cen(&mut self, d: IteratesVector) {
229 self.delta_cen = Some(d);
230 }
231}
232
233/// Convenience handle. Mirrors how upstream passes the data object
234/// around as `SmartPtr<IpoptData>`.
235pub type IpoptDataHandle = Rc<RefCell<IpoptData>>;
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::iterates_vector::IteratesVector;
241 use pounce_linalg::dense_vector::DenseVectorSpace;
242 use pounce_linalg::Vector;
243 use std::rc::Rc as StdRc;
244
245 fn zero_iv() -> IteratesVector {
246 let z = |n| StdRc::new(DenseVectorSpace::new(n).make_new_dense()) as StdRc<dyn Vector>;
247 IteratesVector::new(z(2), z(1), z(1), z(1), z(2), z(2), z(1), z(1))
248 }
249
250 #[test]
251 fn accept_trial_point_promotes_trial_to_curr() {
252 let mut d = IpoptData::new();
253 d.set_trial(zero_iv());
254 assert!(d.curr.is_none());
255 d.accept_trial_point();
256 assert!(d.curr.is_some());
257 assert!(d.trial.is_none());
258 }
259}