Skip to main content

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}