Skip to main content

pounce_algorithm/
intermediate.rs

1//! Per-iteration "intermediate" context shared with downstream
2//! callers (notably the C-API inspector functions
3//! `GetIpoptCurrentIterate` / `GetIpoptCurrentViolations`).
4//!
5//! Mirrors upstream Ipopt's `OrigIpoptNLP::GetIpoptCurrent*` flow: the
6//! main loop installs a snapshot of the algorithm-side state into
7//! thread-local storage immediately before invoking the user's
8//! intermediate callback, and clears it on return. Inspector functions
9//! consult the TLS slot; outside the callback window every accessor
10//! reports "not available".
11//!
12//! The snapshot is intentionally cheap to assemble — we stash `Rc`
13//! handles to `IpoptData`, `IpoptCq`, and the algorithm-side `IpoptNlp`
14//! rather than precomputing every field, so callers that read just one
15//! quantity pay only for what they look at.
16
17use crate::ipopt_cq::IpoptCqHandle;
18use crate::ipopt_data::IpoptDataHandle;
19use crate::ipopt_nlp::IpoptNlp;
20use pounce_common::types::{Index, Number};
21use std::cell::RefCell;
22use std::rc::Rc;
23
24/// Snapshot stashed in TLS for the duration of one
25/// `TNLP::intermediate_callback` invocation.
26#[derive(Clone)]
27pub struct IntermediateContext {
28    pub data: IpoptDataHandle,
29    pub cq: IpoptCqHandle,
30    pub nlp: Rc<RefCell<dyn IpoptNlp>>,
31}
32
33/// The materialised iterate of the user's problem (full `n`, full `m`,
34/// original `g` order), and its window. Defined in
35/// [`pounce_nlp::live_iterate`] so the decorators that lift it one layer
36/// per `intermediate_callback` forward — presolve's above all — can reach
37/// it; re-exported here, where callers have always found it.
38///
39/// The active-set SQP arm installs one at every fire. The interior-point
40/// arm installs one when the TNLP asks for it
41/// ([`pounce_nlp::tnlp::TNLP::wants_current_iterate`]), built by
42/// [`ipm_user_iterate`].
43pub use pounce_nlp::live_iterate::{UserIterate, UserIterateGuard, with_current_user_iterate};
44
45thread_local! {
46    static CURRENT_CTX: RefCell<Option<IntermediateContext>> = const { RefCell::new(None) };
47}
48
49/// The interior-point iterate in the space of the TNLP the NLP wraps, by
50/// the conversions `finalize_via_orig_nlp` applies to the final one, so the
51/// iterate read at the last fire is the solution `finalize_solution`
52/// reports:
53///
54/// * `x` through `finalize_solution_x` (fixed variables spliced back in);
55/// * `z_L`, `z_U`, `λ` through the `finalize_solution_*` lifts, which
56///   divide out `obj_scale_factor` (the unscaled-Lagrangian convention);
57/// * the bound multipliers of variables `make_parameter` removed recovered
58///   from `z_L − z_U = ∇f + Jᵀλ` when `fixed_var_duals` — upstream
59///   `TNLP::get_curr_iterate` does the same through
60///   `TNLPAdapter::ResortBoundMultipliers` for every treatment but
61///   `make_parameter_nodual`. Left at `0` they were not a zero multiplier
62///   but a missing one;
63/// * per-variable `user-scaling` (`x̃ = d ⊙ x`) is *not* undone here: the
64///   `ScalingTnlp` that substitutes it undoes it on the iterate's way
65///   through its `intermediate_callback`, as its `finalize_solution` does;
66/// * `g` from the iterate's own `c` / `d` values, unscaled.
67///
68/// The fixed-variable recovery evaluates the TNLP, so this must run before
69/// the callback borrows it, never from inside the callback.
70pub fn ipm_user_iterate(
71    nlp: &Rc<RefCell<dyn IpoptNlp>>,
72    data: &IpoptDataHandle,
73    cq: &IpoptCqHandle,
74    fixed_var_duals: bool,
75) -> Option<UserIterate> {
76    let curr = data.borrow().curr.clone()?;
77    // `curr_c` / `curr_d` evaluate through the NLP when not cached; no NLP
78    // borrow may be alive across them.
79    let (c, d, glx) = {
80        let cq = cq.borrow();
81        (cq.curr_c(), cq.curr_d(), cq.curr_grad_lag_x())
82    };
83    let nlp = nlp.borrow();
84    let n = nlp.n_full_x() as usize;
85    let m = nlp.n_full_g() as usize;
86    let x = nlp.finalize_solution_x(&*curr.x);
87    let mut z_l = nlp.finalize_solution_z_l(&*curr.z_l);
88    if z_l.is_empty() {
89        z_l = vec![0.0; n];
90    }
91    let mut z_u = nlp.finalize_solution_z_u(&*curr.z_u);
92    if z_u.is_empty() {
93        z_u = vec![0.0; n];
94    }
95    let mut lambda = nlp.finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
96    if lambda.is_empty() {
97        lambda = vec![0.0; m];
98    }
99    if fixed_var_duals {
100        nlp.complete_fixed_var_bound_multipliers(&x, &lambda, &mut z_l, &mut z_u);
101    }
102    let g = nlp.pack_g_for_user(&*c, &*d);
103    let scaling = Some(iterate_scaling(&*nlp));
104    let (x_l_alg, x_u_alg) = algorithm_x_bounds(&*nlp, &x);
105    let grad_lag_x = user_grad_lag(&*nlp, &dense_values(&*glx));
106    Some(UserIterate {
107        x,
108        z_l,
109        z_u,
110        g,
111        lambda,
112        scaling,
113        x_l_alg,
114        x_u_alg,
115        grad_lag_x,
116    })
117}
118
119fn dense_values(v: &dyn pounce_linalg::Vector) -> Vec<Number> {
120    v.as_any()
121        .downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
122        .map(|d| d.expanded_values())
123        .unwrap_or_default()
124}
125
126/// The algorithm's own `∇L` (var-x, scaled: `obj·∇f + J̃ᵀỹ − z̃_L + z̃_U`,
127/// upstream `curr_grad_lag_x`) as the unscaled `∇f + Jᵀλ − z_L + z_U` of the
128/// TNLP `nlp` wraps — `÷ obj`, the row factors cancelling in `J̃ᵀỹ` — at
129/// full x, `0` at a column the algorithm does not hold (upstream
130/// `ResortX(…, usefixedvals=false)`). Its `∇f` and `J` are the ones the
131/// algorithm evaluated at the iterate, so this costs no evaluation.
132pub fn user_grad_lag(nlp: &dyn IpoptNlp, glx: &[Number]) -> Vec<Number> {
133    let n = nlp.n_full_x() as usize;
134    let obj = nlp.obj_scaling_factor();
135    let inv = if obj == 0.0 { 1.0 } else { 1.0 / obj };
136    (0..n)
137        .map(|j| match nlp.full_x_to_var_x(j as Index) {
138            Some(k) => glx.get(k as usize).map_or(Number::NAN, |g| g * inv),
139            None => 0.0,
140        })
141        .collect()
142}
143
144/// The variable bounds `nlp` iterates against — its live `x_L` / `x_U`,
145/// i.e. the declared box after `bound_relax_factor` widened it and any
146/// safe-slack adjustment moved it (upstream `IpoptNLP::x_L()`, what
147/// `curr_slack_x_L` is measured to) — in the shape of the TNLP it wraps
148/// ([`UserIterate::x_l_alg`]): `±∞` at a column with no such bound, and at a
149/// fixed variable the algorithm does not hold, its value `x[j]`.
150pub fn algorithm_x_bounds(nlp: &dyn IpoptNlp, x: &[Number]) -> (Vec<Number>, Vec<Number>) {
151    use pounce_linalg::dense_vector::DenseVectorSpace;
152    let n = nlp.n_full_x() as usize;
153    let side = |live: &dyn pounce_linalg::Vector,
154                upper: bool,
155                packed: &dyn Fn(&dyn pounce_linalg::Vector) -> Vec<Number>| {
156        let mut ones = DenseVectorSpace::new(live.dim()).make_new_dense();
157        if live.dim() > 0 {
158            ones.set_values(&vec![1.0; live.dim() as usize]);
159        }
160        let (vals, mask) = (packed(live), packed(&ones));
161        (0..n)
162            .map(|j| {
163                if nlp.full_x_to_var_x(j as Index).is_none() {
164                    x.get(j).copied().unwrap_or(Number::NAN)
165                } else if mask.get(j).copied().unwrap_or(0.0) != 0.0 {
166                    vals[j]
167                } else if upper {
168                    Number::INFINITY
169                } else {
170                    Number::NEG_INFINITY
171                }
172            })
173            .collect::<Vec<Number>>()
174    };
175    (
176        side(nlp.x_l(), false, &|v| nlp.pack_z_l_for_user(v)),
177        side(nlp.x_u(), true, &|v| nlp.pack_z_u_for_user(v)),
178    )
179}
180
181/// The [`IterateScaling`](pounce_nlp::live_iterate::IterateScaling) of the
182/// problem `nlp` iterates on, in the shape of the TNLP it wraps — the
183/// objective factor and the per-row `c` / `d` factors, and which columns
184/// the algorithm holds (not a fixed variable, which upstream's `ResortX`
185/// reports at the TNLP's own value). The per-variable factor is `1` here:
186/// a `ScalingTnlp` substituting `x̃ = d ⊙ x` multiplies its `d` in when it
187/// lifts the iterate. Both arms solve through the same `OrigIpoptNlp`, so
188/// both read their scaling here.
189pub fn iterate_scaling(nlp: &dyn IpoptNlp) -> pounce_nlp::live_iterate::IterateScaling {
190    let n = nlp.n_full_x() as usize;
191    let m = nlp.n_full_g() as usize;
192    let x_in_alg: Vec<bool> = (0..n)
193        .map(|j| nlp.full_x_to_var_x(j as Index).is_some())
194        .collect();
195    let x = vec![1.0; n];
196    let cs = nlp.c_scale_vec();
197    let ds = nlp.d_scale_vec();
198    let g = (0..m)
199        .map(|i| {
200            if let Some(k) = nlp.full_g_to_c_block(i as Index) {
201                cs.as_ref()
202                    .and_then(|v| v.get(k as usize).copied())
203                    .unwrap_or(1.0)
204            } else if let Some(k) = nlp.full_g_to_d_block(i as Index) {
205                ds.as_ref()
206                    .and_then(|v| v.get(k as usize).copied())
207                    .unwrap_or(1.0)
208            } else {
209                1.0
210            }
211        })
212        .collect();
213    pounce_nlp::live_iterate::IterateScaling {
214        obj: nlp.obj_scaling_factor(),
215        x,
216        x_in_alg,
217        g,
218    }
219}
220
221/// RAII guard — installs `ctx` on construction, clears on drop. Used
222/// by the algorithm to scope visibility of live iterate state to one
223/// callback fire.
224pub struct CtxGuard {
225    _private: (),
226}
227
228impl CtxGuard {
229    pub fn install(ctx: IntermediateContext) -> Self {
230        CURRENT_CTX.with(|c| *c.borrow_mut() = Some(ctx));
231        Self { _private: () }
232    }
233}
234
235impl Drop for CtxGuard {
236    fn drop(&mut self) {
237        CURRENT_CTX.with(|c| *c.borrow_mut() = None);
238    }
239}
240
241/// Read access to the currently installed context. Returns `None`
242/// outside the intermediate-callback window.
243pub fn with_current<F, R>(f: F) -> Option<R>
244where
245    F: FnOnce(&IntermediateContext) -> R,
246{
247    CURRENT_CTX.with(|c| c.borrow().as_ref().map(f))
248}
249
250/// Whether a context is currently installed.
251pub fn is_active() -> bool {
252    CURRENT_CTX.with(|c| c.borrow().is_some()) || pounce_nlp::live_iterate::is_installed()
253}