pounce_nlp/live_iterate.rs
1//! The live iterate of the user's problem, for the intermediate-callback
2//! window (`GetIpoptCurrentIterate` / `GetIpoptCurrentViolations`,
3//! `pounce_algorithm::intermediate::with_current_user_iterate`).
4//!
5//! Upstream Ipopt's `TNLP::get_curr_iterate` reads the algorithm's iterate
6//! and converts it on the spot, through `TNLPAdapter::ResortX` /
7//! `ResortBoundMultipliers` / `ResortG`, into the space the user's TNLP
8//! declared. pounce cannot do that conversion at read time: the reader runs
9//! inside the user's callback, which holds the TNLP mutably, and part of the
10//! conversion — the bound multipliers of fixed variables, the rows and
11//! columns a presolve layer removed — has to *evaluate* that TNLP. So the
12//! algorithm materialises the iterate before the callback, in the space of
13//! the TNLP it solves, and each decorator between that TNLP and the user's
14//! lifts it one layer on its way through `intermediate_callback`, by the
15//! rule its own `finalize_solution` applies. What the user reads at the last
16//! fire is then the point `finalize_solution` reports.
17//!
18//! Lives here rather than in `pounce-algorithm` so the decorators that lift
19//! it (`pounce-presolve`'s, which do not depend on the algorithm crate) can
20//! reach it.
21
22use pounce_common::types::Number;
23use std::cell::RefCell;
24
25/// Iterate of a TNLP in that TNLP's own index spaces (full `n`, full `m`,
26/// its `g` order), in the units `finalize_solution` reports: the
27/// unscaled-Lagrangian multipliers `∇f + Jᵀλ − z_L + z_U = 0`.
28#[derive(Clone, Debug, Default)]
29pub struct UserIterate {
30 pub x: Vec<Number>,
31 pub z_l: Vec<Number>,
32 pub z_u: Vec<Number>,
33 pub g: Vec<Number>,
34 pub lambda: Vec<Number>,
35 /// The scaling of the problem the algorithm solves, entry by entry in
36 /// this iterate's shape (see [`IterateScaling`]); what
37 /// `GetIpoptCurrentIterate` / `GetIpoptCurrentViolations` with
38 /// `scaled = TRUE` multiply by. `None`: not known (the scaled readers
39 /// then report the unscaled values, scale 1).
40 pub scaling: Option<IterateScaling>,
41 /// The variable bounds the algorithm measures its slacks to, in this
42 /// iterate's units and shape: the declared box widened by
43 /// `bound_relax_factor` (and moved by any safe-slack adjustment) for a
44 /// column the algorithm holds; the declared bound for one it does not
45 /// (a fixed variable: its value; a column presolve removed); `±∞` where
46 /// the column has no such bound. Upstream's `compl_x_*` is the
47 /// algorithm's `slack·z`, so it is measured to these. Empty: not known
48 /// (the declared bounds are used). One residual difference: under
49 /// `honor_original_bounds` (off by default) the reported `x` is projected
50 /// into the declared box, so `x − x_L^alg` is the projected point's
51 /// slack, not the algorithm's.
52 pub x_l_alg: Vec<Number>,
53 pub x_u_alg: Vec<Number>,
54 /// `∇f + Jᵀλ − z_L + z_U` at this iterate, unscaled, in this iterate's
55 /// shape, from evaluations the algorithm already made (its own
56 /// `∇L` on the interior-point and SQP arms; presolve layers evaluate the
57 /// inner problem from the iteration's record). `0` at a column the
58 /// algorithm does not hold (upstream `ResortX(…, usefixedvals=false)`).
59 /// Empty: not known (`GetIpoptCurrentViolations` then evaluates).
60 pub grad_lag_x: Vec<Number>,
61}
62
63/// Per-entry factors taking a [`UserIterate`] (and the violations measured
64/// at it) to the space the algorithm iterates in — upstream's
65/// `NLP_scaling()` applied to the TNLP-shaped quantities
66/// `TNLP::get_curr_iterate(scaled = true)` returns:
67///
68/// * `x̃_j = x_scale_j · x_j`, `x_scale` the per-variable factor (`1` for a
69/// column the algorithm does not hold — a fixed variable `make_parameter`
70/// removed, a column presolve eliminated: upstream's `ResortX` writes the
71/// TNLP's own value there);
72/// * `z̃_j = obj · z_j / x_scale_j`, with `z_L` and `z_U` swapped (and
73/// negated) at a column the algorithm does not hold when `obj < 0`, as
74/// upstream does for fixed variables;
75/// * `g̃_i = g_scale_i · g_i`, `λ̃_i = obj · λ_i / g_scale_i` (`1` for a row
76/// the algorithm does not hold).
77#[derive(Clone, Debug, PartialEq)]
78pub struct IterateScaling {
79 /// Objective factor (`obj_scaling_factor`, gradient-based times user).
80 pub obj: Number,
81 /// Per-column factor; length `n`.
82 pub x: Vec<Number>,
83 /// Whether the algorithm holds column `j` (length `n`).
84 pub x_in_alg: Vec<bool>,
85 /// Per-row factor; length `m`.
86 pub g: Vec<Number>,
87}
88
89impl IterateScaling {
90 /// Unit scaling for an `n`-column, `m`-row problem whose columns the
91 /// algorithm all holds.
92 pub fn unit(obj: Number, n: usize, m: usize) -> Self {
93 Self {
94 obj,
95 x: vec![1.0; n],
96 x_in_alg: vec![true; n],
97 g: vec![1.0; m],
98 }
99 }
100
101 fn fits(&self, n: usize, m: usize) -> bool {
102 self.x.len() == n && self.x_in_alg.len() == n && self.g.len() == m
103 }
104}
105
106/// `it` in the algorithm's scaled space, by [`IterateScaling`]'s rule —
107/// upstream `TNLP::get_curr_iterate(scaled = true)`. Unit scaling when the
108/// iterate carries none.
109pub fn scaled_iterate(it: &UserIterate) -> UserIterate {
110 let (n, m) = (it.x.len(), it.g.len());
111 let s = match it.scaling.as_ref().filter(|s| s.fits(n, m)) {
112 Some(s) => s.clone(),
113 None => IterateScaling::unit(1.0, n, m),
114 };
115 let mut out = UserIterate {
116 x: it.x.iter().zip(&s.x).map(|(v, d)| v * d).collect(),
117 z_l: vec![0.0; it.z_l.len()],
118 z_u: vec![0.0; it.z_u.len()],
119 g: it.g.iter().zip(&s.g).map(|(v, d)| v * d).collect(),
120 lambda: it
121 .lambda
122 .iter()
123 .zip(&s.g)
124 .map(|(v, d)| s.obj * v / d)
125 .collect(),
126 scaling: Some(s.clone()),
127 // The algorithm's bounds are scaled like `x`.
128 x_l_alg: it.x_l_alg.iter().zip(&s.x).map(|(v, d)| v * d).collect(),
129 x_u_alg: it.x_u_alg.iter().zip(&s.x).map(|(v, d)| v * d).collect(),
130 grad_lag_x: Vec::new(),
131 };
132 for j in 0..n.min(it.z_l.len()).min(it.z_u.len()) {
133 let f = s.obj / s.x[j];
134 if !s.x_in_alg[j] && s.obj < 0.0 {
135 // Upstream `get_curr_iterate`: "need to swap between z_L and
136 // z_U in this case".
137 out.z_l[j] = -it.z_u[j] * s.obj;
138 out.z_u[j] = -it.z_l[j] * s.obj;
139 } else {
140 out.z_l[j] = it.z_l[j] * f;
141 out.z_u[j] = it.z_u[j] * f;
142 }
143 }
144 out
145}
146
147thread_local! {
148 static CURRENT: RefCell<Option<UserIterate>> = const { RefCell::new(None) };
149}
150
151/// Installs a [`UserIterate`] for the duration of one callback fire and
152/// restores whatever was installed before on drop, so a decorator can
153/// shadow the iterate with its lifted one while it forwards the callback.
154pub struct UserIterateGuard {
155 prev: Option<UserIterate>,
156}
157
158impl UserIterateGuard {
159 pub fn install(it: UserIterate) -> Self {
160 let prev = CURRENT.with(|c| c.borrow_mut().replace(it));
161 Self { prev }
162 }
163}
164
165impl Drop for UserIterateGuard {
166 fn drop(&mut self) {
167 let prev = self.prev.take();
168 CURRENT.with(|c| *c.borrow_mut() = prev);
169 }
170}
171
172/// Read access to the installed iterate; `None` outside a callback window.
173pub fn with_current_user_iterate<F, R>(f: F) -> Option<R>
174where
175 F: FnOnce(&UserIterate) -> R,
176{
177 CURRENT.with(|c| c.borrow().as_ref().map(f))
178}
179
180/// A copy of the installed iterate, for a decorator about to lift it.
181pub fn current_user_iterate() -> Option<UserIterate> {
182 CURRENT.with(|c| c.borrow().clone())
183}
184
185/// Whether an iterate is installed.
186pub fn is_installed() -> bool {
187 CURRENT.with(|c| c.borrow().is_some())
188}
189
190/// Per-entry violations and complementarities at an iterate, in the shape
191/// of upstream `TNLP::get_curr_violations`: full `n` for the first five,
192/// full `m` for the last two, zero where a bound or row does not apply.
193#[derive(Clone, Debug, Default, PartialEq)]
194pub struct Violations {
195 pub x_l_violation: Vec<Number>,
196 pub x_u_violation: Vec<Number>,
197 pub compl_x_l: Vec<Number>,
198 pub compl_x_u: Vec<Number>,
199 pub grad_lag_x: Vec<Number>,
200 pub constraint_violation: Vec<Number>,
201 pub compl_g: Vec<Number>,
202}
203
204/// The declared problem the violations are measured against: the user's
205/// own box and row bounds, with the infinity thresholds that mark a side
206/// absent.
207#[derive(Clone, Copy, Debug)]
208pub struct DeclaredBounds<'a> {
209 pub x_l: &'a [Number],
210 pub x_u: &'a [Number],
211 pub g_l: &'a [Number],
212 pub g_u: &'a [Number],
213 pub lower_inf: Number,
214 pub upper_inf: Number,
215}
216
217/// `∇f + Jᵀλ − z_L + z_U` at `it`, from the gradient and the Jacobian
218/// triplets (0-based) evaluated at `it.x`.
219pub fn lagrangian_gradient(
220 it: &UserIterate,
221 grad_f: &[Number],
222 jac_rows: &[usize],
223 jac_cols: &[usize],
224 jac_vals: &[Number],
225) -> Vec<Number> {
226 let mut s = grad_f.to_vec();
227 for ((&r, &c), &v) in jac_rows.iter().zip(jac_cols).zip(jac_vals) {
228 if let (Some(&l), Some(sc)) = (it.lambda.get(r), s.get_mut(c)) {
229 *sc += v * l;
230 }
231 }
232 for (j, sj) in s.iter_mut().enumerate() {
233 *sj += it.z_u.get(j).copied().unwrap_or(0.0) - it.z_l.get(j).copied().unwrap_or(0.0);
234 }
235 s
236}
237
238/// The unscaled violations of upstream `get_curr_violations(scaled=false)`,
239/// computed from the user-space iterate against the declared problem:
240///
241/// * `x_L_violation = max(0, x_L − x)` (upstream's
242/// `unscaled_curr_orig_x_L_violation`: the *original*, unrelaxed bound);
243/// * `compl_x_L = (x − x_L^alg) · z_L`: upstream's `curr_compl_x_L / obj`,
244/// the algorithm's slack — measured to the bound it iterates against, the
245/// declared one widened by `bound_relax_factor` ([`UserIterate::x_l_alg`])
246/// — times its multiplier. The declared bound when the iterate carries no
247/// algorithm bound for the column. **Not upstream's number under per-variable
248/// user scaling:** Ipopt relaxes the *unscaled* bound before scaling it
249/// (`IpOrigIpoptNLP.cpp`, relax then scale), so its widening is
250/// `min(brf·max(1,|b|), cvt)`; pounce relaxes in `ScalingTnlp`'s `x̃ = d ⊙ x`
251/// frame, `min(brf·max(1,|d·b|), cvt)/d`. They agree at `d = 1`. This
252/// reports pounce's own slack, which is the consistent choice; the
253/// relaxation frame itself predates the live iterate;
254/// * `grad_lag_x = ∇f + Jᵀλ − z_L + z_U` (`grad_lag`; the caller evaluates
255/// it), zeroed at fixed variables when `zero_fixed` — upstream's
256/// `ResortX(…, usefixedvals=false)` under `make_parameter`;
257/// * equality rows: violation `|g − g_L|`, `compl_g = −(g − g_L) · λ`;
258/// inequality rows: violation `max(0, g_L − g, g − g_U)`,
259/// `compl_g = min(λ, 0)(g_L − g) − max(λ, 0)(g − g_U)` over the finite
260/// sides — upstream's signs, taken from `ResortBoundMultipliers`.
261///
262/// A side at or beyond the infinity threshold contributes `0`.
263pub fn user_violations(
264 it: &UserIterate,
265 b: &DeclaredBounds<'_>,
266 grad_lag: Option<Vec<Number>>,
267 zero_fixed: bool,
268) -> Violations {
269 let n = it.x.len();
270 let m = it.g.len();
271 let mut v = Violations {
272 x_l_violation: vec![0.0; n],
273 x_u_violation: vec![0.0; n],
274 compl_x_l: vec![0.0; n],
275 compl_x_u: vec![0.0; n],
276 grad_lag_x: grad_lag.unwrap_or_else(|| vec![0.0; n]),
277 constraint_violation: vec![0.0; m],
278 compl_g: vec![0.0; m],
279 };
280 // The bound the algorithm's slack is measured to, where it has one.
281 let alg = |v: &[Number], j: usize, declared: Number| {
282 v.get(j)
283 .copied()
284 .filter(|b| b.is_finite() && v.len() == n)
285 .unwrap_or(declared)
286 };
287 for j in 0..n {
288 let x = it.x[j];
289 let (lo, up) = (b.x_l[j], b.x_u[j]);
290 if lo > b.lower_inf {
291 v.x_l_violation[j] = (lo - x).max(0.0);
292 v.compl_x_l[j] = (x - alg(&it.x_l_alg, j, lo)) * it.z_l[j];
293 }
294 if up < b.upper_inf {
295 v.x_u_violation[j] = (x - up).max(0.0);
296 v.compl_x_u[j] = (alg(&it.x_u_alg, j, up) - x) * it.z_u[j];
297 }
298 if zero_fixed
299 && lo == up
300 && let Some(gj) = v.grad_lag_x.get_mut(j)
301 {
302 *gj = 0.0;
303 }
304 }
305 for i in 0..m {
306 let (g, lam) = (it.g[i], it.lambda[i]);
307 let (lo, up) = (b.g_l[i], b.g_u[i]);
308 if lo == up {
309 v.constraint_violation[i] = (g - lo).abs();
310 v.compl_g[i] = -(g - lo) * lam;
311 continue;
312 }
313 let mut viol: Number = 0.0;
314 let mut compl = 0.0;
315 if lo > b.lower_inf {
316 viol = viol.max(lo - g);
317 compl += lam.min(0.0) * (lo - g);
318 }
319 if up < b.upper_inf {
320 viol = viol.max(g - up);
321 compl -= lam.max(0.0) * (g - up);
322 }
323 v.constraint_violation[i] = viol;
324 v.compl_g[i] = compl;
325 }
326 v
327}
328
329/// The violations of upstream `get_curr_violations(scaled = true)` from the
330/// unscaled ones at `it`, by the iterate's [`IterateScaling`] (unit when it
331/// carries none) — every entry is a fixed factor of its unscaled
332/// counterpart (`IpTNLP.cpp` 745-1040, `curr_*(…, scaled)`):
333///
334/// * `x_L_violation`, `x_U_violation`: `x_scale ·` (`orig_x_L_violation`,
335/// the declared bound scaled like `x`);
336/// * `compl_x_*`, `compl_g`: `obj ·` (upstream's unscaled ones divide the
337/// algorithm's `s·z`, `c·y` by `obj`; the `d` and row factors cancel);
338/// * `grad_lag_x`: `obj / x_scale ·` (`unapply_grad_obj_scaling`);
339/// * constraint violation: `g_scale ·` (`|c̃|`, and the `d` violation is
340/// formed scaled and unscaled by `unapply_vector_scaling_d`).
341///
342/// One rule for both arms and for presolve, whose removed entries carry
343/// factor 1 (objective factor for the complementarities and `grad_lag_x`).
344pub fn scale_violations(mut v: Violations, it: &UserIterate) -> Violations {
345 let (n, m) = (v.x_l_violation.len(), v.constraint_violation.len());
346 let s = match it.scaling.as_ref().filter(|s| s.fits(n, m)) {
347 Some(s) => s.clone(),
348 None => IterateScaling::unit(1.0, n, m),
349 };
350 for j in 0..n {
351 v.x_l_violation[j] *= s.x[j];
352 v.x_u_violation[j] *= s.x[j];
353 v.compl_x_l[j] *= s.obj;
354 v.compl_x_u[j] *= s.obj;
355 if let Some(g) = v.grad_lag_x.get_mut(j) {
356 *g *= s.obj / s.x[j];
357 }
358 }
359 for i in 0..m {
360 v.constraint_violation[i] *= s.g[i];
361 v.compl_g[i] *= s.obj;
362 }
363 v
364}
365
366/// What `GetIpoptCurrentViolations` reports: [`user_violations`] at `it`,
367/// and with `scaled` [`scale_violations`] of them.
368pub fn current_violations(
369 it: &UserIterate,
370 b: &DeclaredBounds<'_>,
371 grad_lag: Option<Vec<Number>>,
372 zero_fixed: bool,
373 scaled: bool,
374) -> Violations {
375 let v = user_violations(it, b, grad_lag, zero_fixed);
376 if scaled { scale_violations(v, it) } else { v }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 fn it(x: Number) -> UserIterate {
384 UserIterate {
385 x: vec![x],
386 ..Default::default()
387 }
388 }
389
390 /// Upstream `get_curr_iterate(scaled = true)` at a fixed variable under
391 /// a negative objective factor: `z̃_L = −obj·z_U`, `z̃_U = −obj·z_L`;
392 /// at a column the algorithm holds, `obj·z/d` with no swap.
393 #[test]
394 fn a_negative_objective_factor_swaps_only_a_fixed_variables_multipliers() {
395 let it = UserIterate {
396 x: vec![2.0, 1.0],
397 z_l: vec![0.5, 0.0],
398 z_u: vec![0.25, 4.0],
399 g: vec![3.0],
400 lambda: vec![-1.0],
401 scaling: Some(IterateScaling {
402 obj: -2.0,
403 x: vec![4.0, 1.0],
404 x_in_alg: vec![true, false],
405 g: vec![10.0],
406 }),
407 ..Default::default()
408 };
409 let s = scaled_iterate(&it);
410 assert_eq!(s.x, vec![8.0, 1.0]);
411 assert_eq!(s.z_l, vec![-0.25, 8.0]);
412 assert_eq!(s.z_u, vec![-0.125, 0.0]);
413 assert_eq!(s.g, vec![30.0]);
414 assert_eq!(s.lambda, vec![0.2]);
415 }
416
417 #[test]
418 fn a_nested_guard_restores_the_outer_iterate() {
419 assert!(!is_installed());
420 {
421 let _outer = UserIterateGuard::install(it(1.0));
422 {
423 let _inner = UserIterateGuard::install(it(2.0));
424 assert_eq!(with_current_user_iterate(|u| u.x[0]), Some(2.0));
425 }
426 assert_eq!(with_current_user_iterate(|u| u.x[0]), Some(1.0));
427 }
428 assert!(!is_installed());
429 }
430}