Skip to main content

pounce_algorithm/
ipopt_cq.rs

1//! Lazy-cache layer — port of
2//! `Algorithm/IpIpoptCalculatedQuantities.{hpp,cpp}`.
3//!
4//! Upstream's CQ object exposes ~80 cached quantities (`curr_f`,
5//! `curr_grad_f`, `curr_jac_c`, `curr_grad_lag_x`, `curr_compl_*`,
6//! `curr_nlp_error`, etc.). All of them are pure derivations from
7//! `(x, s, y_c, y_d, z_l, z_u, v_l, v_u)` and the NLP function
8//! evaluations.
9//!
10//! Phase 5 ships the priority subset needed by the KKT layer
11//! (Phase 6) and the convergence check / line search (Phase 7).
12//! Caching is intentionally deferred — every accessor recomputes its
13//! value on each call. Tag-based invalidation lands once the inner
14//! loop benchmarks justify the bookkeeping; correctness does not
15//! depend on it.
16//!
17//! All accessors take `&self` and return `Rc<dyn Vector>`. NLP
18//! evaluations require a brief `borrow_mut()` on the Nlp handle;
19//! callers must not hold an outstanding `borrow()` across an
20//! accessor call.
21
22use crate::ipopt_data::IpoptDataHandle;
23use crate::ipopt_nlp::IpoptNlp;
24use crate::iterates_vector::IteratesVector;
25use pounce_common::cached::Cache;
26use pounce_common::types::Number;
27use pounce_linalg::{Matrix, SymMatrix, Vector};
28use std::cell::RefCell;
29use std::rc::Rc;
30
31/// Calculated-quantities object. Holds shared handles on data and the
32/// NLP; per-quantity caches live in `RefCell`s here.
33pub struct IpoptCalculatedQuantities {
34    data: IpoptDataHandle,
35    nlp: Rc<RefCell<dyn IpoptNlp>>,
36
37    /// Optimality scaling cap from `IpOptErrorConvCheck` defaults.
38    pub s_max: Number,
39    /// Damping coefficient for the bound-multiplier complementarity
40    /// term (`kappa_d` in upstream's RegisterOptions).
41    pub kappa_d: Number,
42    /// Correction size for very small slacks (`slack_move` option,
43    /// default `mach_eps^{3/4}`). Drives `calculate_safe_slack`'s
44    /// upper cap on the moved bound — port of upstream's `slack_move_`
45    /// (`IpIpoptCalculatedQuantities.cpp:525`).
46    pub slack_move: Number,
47
48    // Per-iterate caches for the hot accessors used by the KKT solver
49    // dependency-tag check. Without these the PdFullSpaceSolver sees a
50    // fresh tag on every solve (each `curr_slack_*` / `curr_sigma_*`
51    // allocates a new vector with a fresh `TaggedCell`), which forces
52    // an MA57 refactor on every SOC step even though the matrix data
53    // is unchanged. Caches are keyed on the input iterate-vector tag
54    // and survive across calls but are naturally invalidated when the
55    // outer iterate advances (curr.x bump).
56    curr_slack_x_l_cache: RefCell<Cache<Rc<dyn Vector>>>,
57    curr_slack_x_u_cache: RefCell<Cache<Rc<dyn Vector>>>,
58    curr_slack_s_l_cache: RefCell<Cache<Rc<dyn Vector>>>,
59    curr_slack_s_u_cache: RefCell<Cache<Rc<dyn Vector>>>,
60    curr_sigma_x_cache: RefCell<Cache<Rc<dyn Vector>>>,
61    curr_sigma_s_cache: RefCell<Cache<Rc<dyn Vector>>>,
62}
63
64/// Helper: convert `Box<dyn Vector>` to `Rc<dyn Vector>`. Cheap; the
65/// box is unwrapped without copying.
66fn rc_from(v: Box<dyn Vector>) -> Rc<dyn Vector> {
67    Rc::from(v)
68}
69
70/// Result of [`IpoptCalculatedQuantities::adjusted_trial_bounds`]: the
71/// new `x_L / x_U / d_L / d_U` to install on the NLP when one or more
72/// trial slacks were corrected by the safe-slack mechanism.
73pub struct AdjustedBounds {
74    /// Total number of slack components corrected across all four blocks.
75    pub adjusted: usize,
76    pub x_l: Box<dyn Vector>,
77    pub x_u: Box<dyn Vector>,
78    pub d_l: Box<dyn Vector>,
79    pub d_u: Box<dyn Vector>,
80}
81
82impl IpoptCalculatedQuantities {
83    pub fn new(data: IpoptDataHandle, nlp: Rc<RefCell<dyn IpoptNlp>>) -> Self {
84        Self {
85            data,
86            nlp,
87            s_max: 100.0,
88            kappa_d: 1e-5,
89            slack_move: f64::EPSILON.powf(0.75),
90            curr_slack_x_l_cache: RefCell::new(Cache::new(1)),
91            curr_slack_x_u_cache: RefCell::new(Cache::new(1)),
92            curr_slack_s_l_cache: RefCell::new(Cache::new(1)),
93            curr_slack_s_u_cache: RefCell::new(Cache::new(1)),
94            curr_sigma_x_cache: RefCell::new(Cache::new(1)),
95            curr_sigma_s_cache: RefCell::new(Cache::new(1)),
96        }
97    }
98
99    pub fn data(&self) -> &IpoptDataHandle {
100        &self.data
101    }
102
103    pub fn nlp(&self) -> &Rc<RefCell<dyn IpoptNlp>> {
104        &self.nlp
105    }
106
107    pub(crate) fn curr_iv(&self) -> IteratesVector {
108        let Some(iv) = self.data.borrow().curr.as_ref().cloned() else {
109            unreachable!("IpoptCalculatedQuantities: curr iterate not set");
110        };
111        iv
112    }
113
114    fn trial_iv(&self) -> IteratesVector {
115        let Some(iv) = self.data.borrow().trial.as_ref().cloned() else {
116            unreachable!("IpoptCalculatedQuantities: trial iterate not set");
117        };
118        iv
119    }
120
121    // --------------------------------------------------------------
122    // Slacks: s_L = P_L^T x - x_L,  s_U = x_U - P_U^T x.
123    // Mirror of `CalcSlack_L` / `CalcSlack_U`
124    // (`IpIpoptCalculatedQuantities.cpp:238-266`).
125    // --------------------------------------------------------------
126
127    fn calc_slack_l_box(p: &dyn Matrix, x: &dyn Vector, x_bound: &dyn Vector) -> Box<dyn Vector> {
128        let mut result = x_bound.make_new();
129        result.copy(x_bound);
130        // result = -1*result + 1*P^T x  ⇒  P^T x - x_bound.
131        p.trans_mult_vector(1.0, x, -1.0, &mut *result);
132        result
133    }
134
135    fn calc_slack_u_box(p: &dyn Matrix, x: &dyn Vector, x_bound: &dyn Vector) -> Box<dyn Vector> {
136        let mut result = x_bound.make_new();
137        result.copy(x_bound);
138        // result = 1*result + (-1)*P^T x  ⇒  x_bound - P^T x.
139        p.trans_mult_vector(-1.0, x, 1.0, &mut *result);
140        result
141    }
142
143    /// Floor a freshly computed slack against machine precision and,
144    /// where it falls below `eps*min(1,mu)`, raise it to a representable
145    /// positive value, returning the number of corrected components.
146    /// Faithful port of `IpoptCalculatedQuantities::CalculateSafeSlack`
147    /// (`IpIpoptCalculatedQuantities.cpp:455-537`): the corrected slack
148    /// is `min(max(mu/multiplier, s_min), slack_move*max(1,|bound|)+slack)`.
149    /// `multiplier` and `mu` are taken from the *current* iterate, exactly
150    /// as upstream does even for trial slacks.
151    fn calculate_safe_slack(
152        &self,
153        slack: &mut dyn Vector,
154        bound: &dyn Vector,
155        multiplier: &dyn Vector,
156        mu: Number,
157    ) -> usize {
158        if slack.dim() == 0 {
159            return 0;
160        }
161        let min_slack = slack.min();
162        // s_min = eps * min(1, mu); if mu drove it to 0, keep it strictly
163        // positive (upstream #212) so the strict `slack < s_min` test and
164        // the barrier term stay well-defined.
165        let mut s_min = f64::EPSILON * mu.min(1.0);
166        if s_min == 0.0 {
167            s_min = f64::MIN_POSITIVE;
168        }
169        if min_slack >= s_min {
170            return 0;
171        }
172
173        // t = sign(slack - s_min); then collapse to 1 where slack < s_min,
174        // 0 elsewhere.
175        let mut t = slack.make_new();
176        t.copy(&*slack);
177        t.add_scalar(-s_min);
178        t.element_wise_sgn();
179        let mut zero_vec = t.make_new();
180        zero_vec.set(0.0);
181        t.element_wise_min(&*zero_vec); // -1 if slack < s_min, else 0
182        t.scal(-1.0); //  1 if slack < s_min, else 0
183        let retval = t.asum().round() as usize;
184
185        // Clamp the raw slack to be non-negative before forming the target
186        // (upstream's AW fix for negative slacks producing 0).
187        slack.element_wise_max(&*zero_vec);
188
189        // t2 = max(mu/multiplier, s_min) - slack.
190        let mut t2 = t.make_new();
191        let mut s_min_vec = t2.make_new();
192        s_min_vec.set(s_min);
193        if mu != 0.0 {
194            // mu/0 → +inf here, intentionally capped by t_max below.
195            t2.set(mu);
196            t2.element_wise_divide(multiplier);
197            t2.element_wise_max(&*s_min_vec);
198        } else {
199            // mu == 0: max(0/multiplier, s_min) is s_min everywhere, but a 0/0
200            // (zero multiplier at μ=0) would seed the slack target with NaN and
201            // poison the bound move — pin straight to s_min instead.
202            t2.copy(&*s_min_vec);
203        }
204        t2.axpy(-1.0, &*slack);
205
206        // t = max(mu/multiplier, s_min) where flagged, else slack.
207        t.element_wise_select(&*t2);
208        t.axpy(1.0, &*slack);
209
210        // t_max = slack_move*max(1,|bound|) + slack.
211        let mut t_max = t2; // reuse buffer
212        t_max.set(1.0);
213        let mut abs_bound = bound.make_new();
214        abs_bound.copy(bound);
215        abs_bound.element_wise_abs();
216        t_max.element_wise_max(&*abs_bound);
217        // t_max = 1.0*slack + slack_move*t_max.
218        t_max.add_one_vector(1.0, &*slack, self.slack_move);
219
220        // new slack = min(target, t_max) where flagged, else slack.
221        t.element_wise_min(&*t_max);
222        slack.copy(&*t);
223        retval
224    }
225
226    /// `calc_slack_l` followed by `calculate_safe_slack`, returning the
227    /// (floored) slack plus the number of corrected components. The
228    /// multiplier and `mu` come from the current iterate.
229    fn safe_slack_l(
230        &self,
231        p: &dyn Matrix,
232        x: &dyn Vector,
233        bound: &dyn Vector,
234        multiplier: &dyn Vector,
235    ) -> (Rc<dyn Vector>, usize) {
236        let mu = self.data.borrow().curr_mu;
237        let mut result = Self::calc_slack_l_box(p, x, bound);
238        let n = self.calculate_safe_slack(&mut *result, bound, multiplier, mu);
239        (rc_from(result), n)
240    }
241
242    fn safe_slack_u(
243        &self,
244        p: &dyn Matrix,
245        x: &dyn Vector,
246        bound: &dyn Vector,
247        multiplier: &dyn Vector,
248    ) -> (Rc<dyn Vector>, usize) {
249        let mu = self.data.borrow().curr_mu;
250        let mut result = Self::calc_slack_u_box(p, x, bound);
251        let n = self.calculate_safe_slack(&mut *result, bound, multiplier, mu);
252        (rc_from(result), n)
253    }
254
255    pub fn curr_slack_x_l(&self) -> Rc<dyn Vector> {
256        let iv = self.curr_iv();
257        {
258            let cache = self.curr_slack_x_l_cache.borrow();
259            if let Some(v) = cache.get(&[iv.x.as_tagged()], &[]) {
260                return v;
261            }
262        }
263        let nlp = self.nlp.borrow();
264        let (v, _) = self.safe_slack_l(&*nlp.px_l(), &*iv.x, nlp.x_l(), &*iv.z_l);
265        self.curr_slack_x_l_cache
266            .borrow_mut()
267            .add(v.clone(), &[iv.x.as_tagged()], &[]);
268        v
269    }
270
271    pub fn curr_slack_x_u(&self) -> Rc<dyn Vector> {
272        let iv = self.curr_iv();
273        {
274            let cache = self.curr_slack_x_u_cache.borrow();
275            if let Some(v) = cache.get(&[iv.x.as_tagged()], &[]) {
276                return v;
277            }
278        }
279        let nlp = self.nlp.borrow();
280        let (v, _) = self.safe_slack_u(&*nlp.px_u(), &*iv.x, nlp.x_u(), &*iv.z_u);
281        self.curr_slack_x_u_cache
282            .borrow_mut()
283            .add(v.clone(), &[iv.x.as_tagged()], &[]);
284        v
285    }
286
287    pub fn curr_slack_s_l(&self) -> Rc<dyn Vector> {
288        let iv = self.curr_iv();
289        {
290            let cache = self.curr_slack_s_l_cache.borrow();
291            if let Some(v) = cache.get(&[iv.s.as_tagged()], &[]) {
292                return v;
293            }
294        }
295        let nlp = self.nlp.borrow();
296        let (v, _) = self.safe_slack_l(&*nlp.pd_l(), &*iv.s, nlp.d_l(), &*iv.v_l);
297        self.curr_slack_s_l_cache
298            .borrow_mut()
299            .add(v.clone(), &[iv.s.as_tagged()], &[]);
300        v
301    }
302
303    pub fn curr_slack_s_u(&self) -> Rc<dyn Vector> {
304        let iv = self.curr_iv();
305        {
306            let cache = self.curr_slack_s_u_cache.borrow();
307            if let Some(v) = cache.get(&[iv.s.as_tagged()], &[]) {
308                return v;
309            }
310        }
311        let nlp = self.nlp.borrow();
312        let (v, _) = self.safe_slack_u(&*nlp.pd_u(), &*iv.s, nlp.d_u(), &*iv.v_u);
313        self.curr_slack_s_u_cache
314            .borrow_mut()
315            .add(v.clone(), &[iv.s.as_tagged()], &[]);
316        v
317    }
318
319    pub fn trial_slack_x_l(&self) -> Rc<dyn Vector> {
320        let iv = self.trial_iv();
321        let mult = self.curr_iv();
322        let nlp = self.nlp.borrow();
323        self.safe_slack_l(&*nlp.px_l(), &*iv.x, nlp.x_l(), &*mult.z_l)
324            .0
325    }
326
327    pub fn trial_slack_x_u(&self) -> Rc<dyn Vector> {
328        let iv = self.trial_iv();
329        let mult = self.curr_iv();
330        let nlp = self.nlp.borrow();
331        self.safe_slack_u(&*nlp.px_u(), &*iv.x, nlp.x_u(), &*mult.z_u)
332            .0
333    }
334
335    pub fn trial_slack_s_l(&self) -> Rc<dyn Vector> {
336        let iv = self.trial_iv();
337        let mult = self.curr_iv();
338        let nlp = self.nlp.borrow();
339        self.safe_slack_l(&*nlp.pd_l(), &*iv.s, nlp.d_l(), &*mult.v_l)
340            .0
341    }
342
343    pub fn trial_slack_s_u(&self) -> Rc<dyn Vector> {
344        let iv = self.trial_iv();
345        let mult = self.curr_iv();
346        let nlp = self.nlp.borrow();
347        self.safe_slack_u(&*nlp.pd_u(), &*iv.s, nlp.d_u(), &*mult.v_u)
348            .0
349    }
350
351    /// Compute the four trial slacks with safe-slack flooring and, if any
352    /// component was corrected, the adjusted variable bounds that make the
353    /// trial slacks exactly representable. Port of the bound-adjustment
354    /// block in `IpoptAlgorithm::AcceptTrialPoint`
355    /// (`IpIpoptAlg.cpp:664-706`): `new_x_L = Px_L^T x - safe_slack_x_L`,
356    /// `new_x_U = Px_U^T x + safe_slack_x_U`, likewise for `s`/`d`.
357    /// Returns `None` when no slack needed correcting.
358    pub fn adjusted_trial_bounds(&self) -> Option<AdjustedBounds> {
359        let iv = self.trial_iv();
360        let mult = self.curr_iv();
361        let nlp = self.nlp.borrow();
362
363        let (s_x_l, n_x_l) = self.safe_slack_l(&*nlp.px_l(), &*iv.x, nlp.x_l(), &*mult.z_l);
364        let (s_x_u, n_x_u) = self.safe_slack_u(&*nlp.px_u(), &*iv.x, nlp.x_u(), &*mult.z_u);
365        let (s_s_l, n_s_l) = self.safe_slack_l(&*nlp.pd_l(), &*iv.s, nlp.d_l(), &*mult.v_l);
366        let (s_s_u, n_s_u) = self.safe_slack_u(&*nlp.pd_u(), &*iv.s, nlp.d_u(), &*mult.v_u);
367
368        let adjusted = n_x_l + n_x_u + n_s_l + n_s_u;
369        if adjusted == 0 {
370            return None;
371        }
372
373        // new_x_L = Px_L^T x - safe_slack_x_L
374        let mut new_x_l = nlp.x_l().make_new();
375        nlp.px_l()
376            .trans_mult_vector(1.0, &*iv.x, 0.0, &mut *new_x_l);
377        new_x_l.axpy(-1.0, &*s_x_l);
378        // new_x_U = Px_U^T x + safe_slack_x_U
379        let mut new_x_u = nlp.x_u().make_new();
380        nlp.px_u()
381            .trans_mult_vector(1.0, &*iv.x, 0.0, &mut *new_x_u);
382        new_x_u.axpy(1.0, &*s_x_u);
383        // new_d_L = Pd_L^T s - safe_slack_s_L
384        let mut new_d_l = nlp.d_l().make_new();
385        nlp.pd_l()
386            .trans_mult_vector(1.0, &*iv.s, 0.0, &mut *new_d_l);
387        new_d_l.axpy(-1.0, &*s_s_l);
388        // new_d_U = Pd_U^T s + safe_slack_s_U
389        let mut new_d_u = nlp.d_u().make_new();
390        nlp.pd_u()
391            .trans_mult_vector(1.0, &*iv.s, 0.0, &mut *new_d_u);
392        new_d_u.axpy(1.0, &*s_s_u);
393
394        Some(AdjustedBounds {
395            adjusted,
396            x_l: new_x_l,
397            x_u: new_x_u,
398            d_l: new_d_l,
399            d_u: new_d_u,
400        })
401    }
402
403    // --------------------------------------------------------------
404    // NLP function evaluations.
405    // --------------------------------------------------------------
406
407    pub fn curr_grad_f(&self) -> Rc<dyn Vector> {
408        let iv = self.curr_iv();
409        let mut nlp = self.nlp.borrow_mut();
410        let mut g = iv.x.make_new();
411        nlp.eval_grad_f(&*iv.x, &mut *g);
412        rc_from(g)
413    }
414
415    pub fn trial_grad_f(&self) -> Rc<dyn Vector> {
416        let iv = self.trial_iv();
417        let mut nlp = self.nlp.borrow_mut();
418        let mut g = iv.x.make_new();
419        nlp.eval_grad_f(&*iv.x, &mut *g);
420        rc_from(g)
421    }
422
423    pub fn curr_c(&self) -> Rc<dyn Vector> {
424        let iv = self.curr_iv();
425        let m = self.nlp.borrow().m_eq();
426        let mut nlp = self.nlp.borrow_mut();
427        let mut c = iv.y_c.make_new();
428        debug_assert_eq!(c.dim(), m);
429        nlp.eval_c(&*iv.x, &mut *c);
430        rc_from(c)
431    }
432
433    pub fn trial_c(&self) -> Rc<dyn Vector> {
434        let iv = self.trial_iv();
435        let mut nlp = self.nlp.borrow_mut();
436        let mut c = iv.y_c.make_new();
437        nlp.eval_c(&*iv.x, &mut *c);
438        rc_from(c)
439    }
440
441    pub fn curr_d(&self) -> Rc<dyn Vector> {
442        let iv = self.curr_iv();
443        let mut nlp = self.nlp.borrow_mut();
444        let mut d = iv.s.make_new();
445        nlp.eval_d(&*iv.x, &mut *d);
446        rc_from(d)
447    }
448
449    pub fn trial_d(&self) -> Rc<dyn Vector> {
450        let iv = self.trial_iv();
451        let mut nlp = self.nlp.borrow_mut();
452        let mut d = iv.s.make_new();
453        nlp.eval_d(&*iv.x, &mut *d);
454        rc_from(d)
455    }
456
457    pub fn curr_jac_c(&self) -> Rc<dyn Matrix> {
458        let iv = self.curr_iv();
459        self.nlp.borrow_mut().eval_jac_c(&*iv.x)
460    }
461
462    pub fn curr_jac_d(&self) -> Rc<dyn Matrix> {
463        let iv = self.curr_iv();
464        self.nlp.borrow_mut().eval_jac_d(&*iv.x)
465    }
466
467    pub fn curr_exact_hessian(&self) -> Rc<dyn SymMatrix> {
468        let iv = self.curr_iv();
469        self.nlp
470            .borrow_mut()
471            .eval_h(&*iv.x, 1.0, &*iv.y_c, &*iv.y_d)
472    }
473
474    /// `curr_d - s` — port of `IpIpoptCalculatedQuantities.cpp:1185-1206`.
475    pub fn curr_d_minus_s(&self) -> Rc<dyn Vector> {
476        let iv = self.curr_iv();
477        let d = self.curr_d();
478        let mut tmp = iv.s.make_new();
479        // tmp = 0*tmp + 1*d + (-1)*s
480        tmp.add_two_vectors(1.0, &*d, -1.0, &*iv.s, 0.0);
481        rc_from(tmp)
482    }
483
484    pub fn trial_d_minus_s(&self) -> Rc<dyn Vector> {
485        let iv = self.trial_iv();
486        let d = self.trial_d();
487        let mut tmp = iv.s.make_new();
488        tmp.add_two_vectors(1.0, &*d, -1.0, &*iv.s, 0.0);
489        rc_from(tmp)
490    }
491
492    /// `J_c^T y_c` — for a generic `vec` argument
493    /// (`IpIpoptCalculatedQuantities.cpp:1373-1404`).
494    pub fn curr_jac_c_t_times_vec(&self, vec: &dyn Vector) -> Rc<dyn Vector> {
495        let iv = self.curr_iv();
496        let jac_c = self.curr_jac_c();
497        let mut tmp = iv.x.make_new();
498        jac_c.trans_mult_vector(1.0, vec, 0.0, &mut *tmp);
499        rc_from(tmp)
500    }
501
502    /// `J_d^T y_d` for arbitrary `vec`.
503    pub fn curr_jac_d_t_times_vec(&self, vec: &dyn Vector) -> Rc<dyn Vector> {
504        let iv = self.curr_iv();
505        let jac_d = self.curr_jac_d();
506        let mut tmp = iv.x.make_new();
507        jac_d.trans_mult_vector(1.0, vec, 0.0, &mut *tmp);
508        rc_from(tmp)
509    }
510
511    pub fn curr_jac_c_t_times_curr_y_c(&self) -> Rc<dyn Vector> {
512        let iv = self.curr_iv();
513        self.curr_jac_c_t_times_vec(&*iv.y_c)
514    }
515
516    pub fn curr_jac_d_t_times_curr_y_d(&self) -> Rc<dyn Vector> {
517        let iv = self.curr_iv();
518        self.curr_jac_d_t_times_vec(&*iv.y_d)
519    }
520
521    /// `J_c v` — `IpIpoptCalculatedQuantities.cpp:1303-1321`.
522    pub fn curr_jac_c_times_vec(&self, vec: &dyn Vector) -> Rc<dyn Vector> {
523        let iv = self.curr_iv();
524        let jac_c = self.curr_jac_c();
525        let mut tmp = iv.y_c.make_new();
526        jac_c.mult_vector(1.0, vec, 0.0, &mut *tmp);
527        rc_from(tmp)
528    }
529
530    /// `J_d v` — `IpIpoptCalculatedQuantities.cpp:1323-1343`.
531    pub fn curr_jac_d_times_vec(&self, vec: &dyn Vector) -> Rc<dyn Vector> {
532        let iv = self.curr_iv();
533        let jac_d = self.curr_jac_d();
534        let mut tmp = iv.s.make_new();
535        jac_d.mult_vector(1.0, vec, 0.0, &mut *tmp);
536        rc_from(tmp)
537    }
538
539    // --------------------------------------------------------------
540    // Lagrangian gradients
541    // --------------------------------------------------------------
542
543    /// `∇_x L = ∇f(x) + J_c^T y_c + J_d^T y_d - P_L z_L + P_U z_U`
544    /// per `IpIpoptCalculatedQuantities.cpp:1993-2030`.
545    pub fn curr_grad_lag_x(&self) -> Rc<dyn Vector> {
546        let iv = self.curr_iv();
547        let grad_f = self.curr_grad_f();
548        let jc_t_y_c = self.curr_jac_c_t_times_curr_y_c();
549        let jd_t_y_d = self.curr_jac_d_t_times_curr_y_d();
550
551        let mut tmp = iv.x.make_new();
552        tmp.copy(&*grad_f);
553        tmp.add_two_vectors(1.0, &*jc_t_y_c, 1.0, &*jd_t_y_d, 1.0);
554
555        let nlp = self.nlp.borrow();
556        nlp.px_l().mult_vector(-1.0, &*iv.z_l, 1.0, &mut *tmp);
557        nlp.px_u().mult_vector(1.0, &*iv.z_u, 1.0, &mut *tmp);
558        rc_from(tmp)
559    }
560
561    /// `∇_s L = -y_d - P_L v_L + P_U v_U`
562    /// (`IpIpoptCalculatedQuantities.cpp:2069-2098`).
563    pub fn curr_grad_lag_s(&self) -> Rc<dyn Vector> {
564        let iv = self.curr_iv();
565        let mut tmp = iv.y_d.make_new();
566        let nlp = self.nlp.borrow();
567        // tmp = P_U v_U
568        nlp.pd_u().mult_vector(1.0, &*iv.v_u, 0.0, &mut *tmp);
569        // tmp = tmp - P_L v_L
570        nlp.pd_l().mult_vector(-1.0, &*iv.v_l, 1.0, &mut *tmp);
571        // tmp = tmp - y_d
572        tmp.axpy(-1.0, &*iv.y_d);
573        rc_from(tmp)
574    }
575
576    // --------------------------------------------------------------
577    // Complementarity (slack ⊙ multiplier)
578    // --------------------------------------------------------------
579
580    fn calc_compl(slack: &dyn Vector, mult: &dyn Vector) -> Rc<dyn Vector> {
581        let mut result = slack.make_new();
582        result.copy(slack);
583        result.element_wise_multiply(mult);
584        rc_from(result)
585    }
586
587    pub fn curr_compl_x_l(&self) -> Rc<dyn Vector> {
588        let slack = self.curr_slack_x_l();
589        let z_l = self.curr_iv().z_l;
590        Self::calc_compl(&*slack, &*z_l)
591    }
592
593    pub fn curr_compl_x_u(&self) -> Rc<dyn Vector> {
594        let slack = self.curr_slack_x_u();
595        let z_u = self.curr_iv().z_u;
596        Self::calc_compl(&*slack, &*z_u)
597    }
598
599    pub fn curr_compl_s_l(&self) -> Rc<dyn Vector> {
600        let slack = self.curr_slack_s_l();
601        let v_l = self.curr_iv().v_l;
602        Self::calc_compl(&*slack, &*v_l)
603    }
604
605    pub fn curr_compl_s_u(&self) -> Rc<dyn Vector> {
606        let slack = self.curr_slack_s_u();
607        let v_u = self.curr_iv().v_u;
608        Self::calc_compl(&*slack, &*v_u)
609    }
610
611    /// `s_L .* z_L - mu` — relaxed complementarity used in the KKT
612    /// RHS. `IpIpoptCalculatedQuantities.cpp:2406-2430`.
613    pub fn curr_relaxed_compl_x_l(&self) -> Rc<dyn Vector> {
614        let mu = self.data.borrow().curr_mu;
615        let mut r = self.curr_compl_x_l().make_new();
616        r.copy(&*self.curr_compl_x_l());
617        r.add_scalar(-mu);
618        rc_from(r)
619    }
620
621    pub fn curr_relaxed_compl_x_u(&self) -> Rc<dyn Vector> {
622        let mu = self.data.borrow().curr_mu;
623        let mut r = self.curr_compl_x_u().make_new();
624        r.copy(&*self.curr_compl_x_u());
625        r.add_scalar(-mu);
626        rc_from(r)
627    }
628
629    pub fn curr_relaxed_compl_s_l(&self) -> Rc<dyn Vector> {
630        let mu = self.data.borrow().curr_mu;
631        let mut r = self.curr_compl_s_l().make_new();
632        r.copy(&*self.curr_compl_s_l());
633        r.add_scalar(-mu);
634        rc_from(r)
635    }
636
637    pub fn curr_relaxed_compl_s_u(&self) -> Rc<dyn Vector> {
638        let mu = self.data.borrow().curr_mu;
639        let mut r = self.curr_compl_s_u().make_new();
640        r.copy(&*self.curr_compl_s_u());
641        r.add_scalar(-mu);
642        rc_from(r)
643    }
644
645    // --------------------------------------------------------------
646    // Σ_x / Σ_s (barrier-Hessian diagonals fed to the augmented system)
647    // `IpIpoptCalculatedQuantities.cpp:3501-3551`.
648    //
649    //   Σ_x = P_L · diag(z_L / s_L) · P_L^T + P_U · diag(z_U / s_U) · P_U^T
650    //   Σ_s = P_L · diag(v_L / s_L) · P_L^T + P_U · diag(v_U / s_U) · P_U^T
651    // --------------------------------------------------------------
652
653    pub fn curr_sigma_x(&self) -> Rc<dyn Vector> {
654        let iv = self.curr_iv();
655        {
656            let cache = self.curr_sigma_x_cache.borrow();
657            if let Some(v) = cache.get(
658                &[iv.x.as_tagged(), iv.z_l.as_tagged(), iv.z_u.as_tagged()],
659                &[],
660            ) {
661                return v;
662            }
663        }
664        let slack_l = self.curr_slack_x_l();
665        let slack_u = self.curr_slack_x_u();
666
667        let mut sigma = iv.x.make_new();
668        sigma.set(0.0);
669
670        let nlp = self.nlp.borrow();
671        nlp.px_l()
672            .add_m_sinv_z(1.0, &*slack_l, &*iv.z_l, &mut *sigma);
673        nlp.px_u()
674            .add_m_sinv_z(1.0, &*slack_u, &*iv.z_u, &mut *sigma);
675        let v = rc_from(sigma);
676        self.curr_sigma_x_cache.borrow_mut().add(
677            v.clone(),
678            &[iv.x.as_tagged(), iv.z_l.as_tagged(), iv.z_u.as_tagged()],
679            &[],
680        );
681        v
682    }
683
684    pub fn curr_sigma_s(&self) -> Rc<dyn Vector> {
685        let iv = self.curr_iv();
686        {
687            let cache = self.curr_sigma_s_cache.borrow();
688            if let Some(v) = cache.get(
689                &[iv.s.as_tagged(), iv.v_l.as_tagged(), iv.v_u.as_tagged()],
690                &[],
691            ) {
692                return v;
693            }
694        }
695        let slack_l = self.curr_slack_s_l();
696        let slack_u = self.curr_slack_s_u();
697
698        let mut sigma = iv.s.make_new();
699        sigma.set(0.0);
700
701        let nlp = self.nlp.borrow();
702        nlp.pd_l()
703            .add_m_sinv_z(1.0, &*slack_l, &*iv.v_l, &mut *sigma);
704        nlp.pd_u()
705            .add_m_sinv_z(1.0, &*slack_u, &*iv.v_u, &mut *sigma);
706        let v = rc_from(sigma);
707        self.curr_sigma_s_cache.borrow_mut().add(
708            v.clone(),
709            &[iv.s.as_tagged(), iv.v_l.as_tagged(), iv.v_u.as_tagged()],
710            &[],
711        );
712        v
713    }
714
715    // --------------------------------------------------------------
716    // Objective f and barrier objective phi
717    // (`IpIpoptCalculatedQuantities.cpp:CalcBarrierTerm`,
718    //  lines 870-1042 in upstream).
719    //
720    //   phi(x,s) = f(x)
721    //              − μ · [Σ ln(s_x_L) + Σ ln(s_x_U)
722    //                   + Σ ln(s_s_L) + Σ ln(s_s_U)]
723    //              + κ_d · μ · [s_x_L · 1_singly_x_L
724    //                          + s_x_U · 1_singly_x_U
725    //                          + s_s_L · 1_singly_s_L
726    //                          + s_s_U · 1_singly_s_U]
727    //
728    // The damping piece vanishes when `kappa_d == 0` (default).
729    // --------------------------------------------------------------
730
731    pub fn curr_f(&self) -> Number {
732        let iv = self.curr_iv();
733        let mut nlp = self.nlp.borrow_mut();
734        nlp.eval_f(&*iv.x)
735    }
736
737    /// Unscaled objective at the current iterate. `curr_f` returns the
738    /// internally scaled value (`f · df_`); upstream IPOPT prints the
739    /// unscaled objective in its iteration log, so this divides the
740    /// scaling back out. Mirrors `IpoptCalculatedQuantities::
741    /// unscaled_curr_f`. A zero factor (scaling never determined) is
742    /// treated as the identity.
743    pub fn unscaled_curr_f(&self) -> Number {
744        let scaled = self.curr_f();
745        let factor = self.nlp.borrow().obj_scaling_factor();
746        if factor == 0.0 {
747            scaled
748        } else {
749            scaled / factor
750        }
751    }
752
753    pub fn trial_f(&self) -> Number {
754        let iv = self.trial_iv();
755        let mut nlp = self.nlp.borrow_mut();
756        nlp.eval_f(&*iv.x)
757    }
758
759    fn barrier_obj_at(
760        &self,
761        f: Number,
762        s_x_l: &dyn Vector,
763        s_x_u: &dyn Vector,
764        s_s_l: &dyn Vector,
765        s_s_u: &dyn Vector,
766    ) -> Number {
767        let mu = self.data.borrow().curr_mu;
768        let log_sum = s_x_l.sum_logs() + s_x_u.sum_logs() + s_s_l.sum_logs() + s_s_u.sum_logs();
769        let mut phi = f - mu * log_sum;
770        if self.kappa_d > 0.0 {
771            let di = self.damping_indicators();
772            phi += self.kappa_d * mu * s_x_l.dot(&*di.x_l);
773            phi += self.kappa_d * mu * s_x_u.dot(&*di.x_u);
774            phi += self.kappa_d * mu * s_s_l.dot(&*di.s_l);
775            phi += self.kappa_d * mu * s_s_u.dot(&*di.s_u);
776        }
777        phi
778    }
779
780    pub fn curr_barrier_obj(&self) -> Number {
781        let f = self.curr_f();
782        let s_x_l = self.curr_slack_x_l();
783        let s_x_u = self.curr_slack_x_u();
784        let s_s_l = self.curr_slack_s_l();
785        let s_s_u = self.curr_slack_s_u();
786        self.barrier_obj_at(f, &*s_x_l, &*s_x_u, &*s_s_l, &*s_s_u)
787    }
788
789    pub fn trial_barrier_obj(&self) -> Number {
790        let f = self.trial_f();
791        let s_x_l = self.trial_slack_x_l();
792        let s_x_u = self.trial_slack_x_u();
793        let s_s_l = self.trial_slack_s_l();
794        let s_s_u = self.trial_slack_s_u();
795        self.barrier_obj_at(f, &*s_x_l, &*s_x_u, &*s_s_l, &*s_s_u)
796    }
797
798    /// Gradient of the barrier objective wrt `x`:
799    ///   ∇_x φ = ∇f(x) − μ · [P_L · (1/s_L) − P_U · (1/s_U)] + damping
800    /// Mirrors `IpIpoptCalculatedQuantities.cpp:CalcGradBarrierObjectiveX`.
801    pub fn curr_grad_barrier_obj_x(&self) -> Rc<dyn Vector> {
802        let iv = self.curr_iv();
803        let mu = self.data.borrow().curr_mu;
804        let s_l = self.curr_slack_x_l();
805        let s_u = self.curr_slack_x_u();
806
807        let mut inv_s_l = s_l.make_new();
808        inv_s_l.copy(&*s_l);
809        inv_s_l.element_wise_reciprocal();
810        let mut inv_s_u = s_u.make_new();
811        inv_s_u.copy(&*s_u);
812        inv_s_u.element_wise_reciprocal();
813
814        let grad_f = self.curr_grad_f();
815        let mut tmp = iv.x.make_new();
816        tmp.copy(&*grad_f);
817        let nlp = self.nlp.borrow();
818        // tmp -= μ · P_L · inv_s_l
819        nlp.px_l().mult_vector(-mu, &*inv_s_l, 1.0, &mut *tmp);
820        // tmp += μ · P_U · inv_s_u
821        nlp.px_u().mult_vector(mu, &*inv_s_u, 1.0, &mut *tmp);
822
823        if self.kappa_d > 0.0 {
824            let di = self.damping_indicators();
825            // + κ_d μ · P_L · 1_singly_x_L
826            nlp.px_l()
827                .mult_vector(self.kappa_d * mu, &*di.x_l, 1.0, &mut *tmp);
828            // − κ_d μ · P_U · 1_singly_x_U
829            nlp.px_u()
830                .mult_vector(-self.kappa_d * mu, &*di.x_u, 1.0, &mut *tmp);
831        }
832        rc_from(tmp)
833    }
834
835    /// Gradient of the barrier objective wrt `s`:
836    ///   ∇_s φ = − μ · [P_L · (1/s_s_L) − P_U · (1/s_s_U)] + damping
837    pub fn curr_grad_barrier_obj_s(&self) -> Rc<dyn Vector> {
838        let iv = self.curr_iv();
839        let mu = self.data.borrow().curr_mu;
840        let s_l = self.curr_slack_s_l();
841        let s_u = self.curr_slack_s_u();
842
843        let mut inv_s_l = s_l.make_new();
844        inv_s_l.copy(&*s_l);
845        inv_s_l.element_wise_reciprocal();
846        let mut inv_s_u = s_u.make_new();
847        inv_s_u.copy(&*s_u);
848        inv_s_u.element_wise_reciprocal();
849
850        let mut tmp = iv.s.make_new();
851        tmp.set(0.0);
852        let nlp = self.nlp.borrow();
853        nlp.pd_l().mult_vector(-mu, &*inv_s_l, 1.0, &mut *tmp);
854        nlp.pd_u().mult_vector(mu, &*inv_s_u, 1.0, &mut *tmp);
855
856        if self.kappa_d > 0.0 {
857            let di = self.damping_indicators();
858            nlp.pd_l()
859                .mult_vector(self.kappa_d * mu, &*di.s_l, 1.0, &mut *tmp);
860            nlp.pd_u()
861                .mult_vector(-self.kappa_d * mu, &*di.s_u, 1.0, &mut *tmp);
862        }
863        rc_from(tmp)
864    }
865
866    // --------------------------------------------------------------
867    // Step-aware quadratic-model quantities — used by the penalty
868    // line-search acceptor's pred/ared test and by the quality-
869    // function mu oracle's q(σ) evaluator.
870    // --------------------------------------------------------------
871
872    /// Directional derivative of the barrier objective along `(δx, δs)`:
873    /// `gradBarrTDelta = ∇_x φ · δx + ∇_s φ · δs`. Port of
874    /// `IpIpoptCalculatedQuantities.cpp:CurrGradBarrTDelta` (called
875    /// `IpCq().curr_gradBarrTDelta()` in upstream after the search dir
876    /// has been computed).
877    pub fn curr_grad_barr_t_delta(&self, delta_x: &dyn Vector, delta_s: &dyn Vector) -> Number {
878        let g_x = self.curr_grad_barrier_obj_x();
879        let g_s = self.curr_grad_barrier_obj_s();
880        g_x.dot(delta_x) + g_s.dot(delta_s)
881    }
882
883    /// `δᵀ(W + Σ_x + δ_pert_x I)δ_x + δ_sᵀ(Σ_s + δ_pert_s I)δ_s` —
884    /// the quadratic-model term used by `IpPenaltyLSAcceptor.cpp:
885    /// InitThisLineSearch:101-129`. Reads `W` and the active PD
886    /// perturbations from [`crate::ipopt_data::IpoptData`].
887    /// Returns 0 if the result would be negative (matching upstream's
888    /// `if dWd <= 0 then dWd = 0` guard at line 133).
889    pub fn curr_dwd(&self, delta_x: &dyn Vector, delta_s: &dyn Vector) -> Number {
890        let mut dwd: Number = 0.0;
891
892        // δ_xᵀ W δ_x.
893        if let Some(w) = self.data.borrow().w.clone() {
894            let mut wd = delta_x.make_new();
895            w.mult_vector(1.0, delta_x, 0.0, &mut *wd);
896            dwd += wd.dot(delta_x);
897        }
898
899        // δ_xᵀ Σ_x δ_x.
900        let sigma_x = self.curr_sigma_x();
901        let mut tmp_x = delta_x.make_new();
902        tmp_x.copy(delta_x);
903        tmp_x.element_wise_multiply(&*sigma_x);
904        dwd += tmp_x.dot(delta_x);
905
906        // δ_sᵀ Σ_s δ_s.
907        let sigma_s = self.curr_sigma_s();
908        let mut tmp_s = delta_s.make_new();
909        tmp_s.copy(delta_s);
910        tmp_s.element_wise_multiply(&*sigma_s);
911        dwd += tmp_s.dot(delta_s);
912
913        // PD perturbations.
914        let pert = self.data.borrow().perturbations;
915        if pert.delta_x != 0.0 {
916            let nx = delta_x.nrm2();
917            dwd += pert.delta_x * nx * nx;
918        }
919        if pert.delta_s != 0.0 {
920            let ns = delta_s.nrm2();
921            dwd += pert.delta_s * ns * ns;
922        }
923
924        dwd.max(0.0)
925    }
926
927    // --------------------------------------------------------------
928    // Constraint violation theta — port of
929    // `IpIpoptCalculatedQuantities.cpp:CurrConstraintViolation`.
930    // Default norm is 1-norm (option `constraint_violation_norm`,
931    // default "1-norm" upstream); we hardwire 1-norm in v1.0.
932    // --------------------------------------------------------------
933
934    pub fn curr_constraint_violation(&self) -> Number {
935        let c = self.curr_c();
936        let dms = self.curr_d_minus_s();
937        c.asum() + dms.asum()
938    }
939
940    pub fn trial_constraint_violation(&self) -> Number {
941        let c = self.trial_c();
942        let dms = self.trial_d_minus_s();
943        c.asum() + dms.asum()
944    }
945
946    /// Max-norm primal infeasibility — `max(||c||_∞, ||d − s||_∞)`. Used
947    /// by the iteration output's `inf_pr` column when
948    /// `inf_pr_output == INTERNAL`. Mirrors
949    /// `IpIpoptCalculatedQuantities.cpp:CurrPrimalInfeasibility(NORM_MAX)`.
950    pub fn curr_primal_infeasibility_max(&self) -> Number {
951        let c = self.curr_c();
952        let dms = self.curr_d_minus_s();
953        c.amax().max(dms.amax())
954    }
955
956    /// Max-norm dual infeasibility — `max(||∇_x L||_∞, ||∇_s L||_∞)`.
957    /// Mirrors `IpIpoptCalculatedQuantities.cpp:CurrDualInfeasibility(NORM_MAX)`.
958    pub fn curr_dual_infeasibility_max(&self) -> Number {
959        let glx = self.curr_grad_lag_x();
960        let gls = self.curr_grad_lag_s();
961        glx.amax().max(gls.amax())
962    }
963
964    /// Scaled stationarity of the infeasibility measure `½‖(c, d−s)‖²`
965    /// — `‖J_cᵀ c + J_dᵀ (d−s)‖_∞ / max(1, ‖(c, d−s)‖_∞)`. The
966    /// numerator is the x-gradient of the squared constraint
967    /// violation; a value near zero with the violation itself bounded
968    /// away from zero marks an iterate converging to a stationary
969    /// point of the infeasibility — i.e. a locally infeasible problem.
970    /// No linear solve: two transpose-products. Mirrors the gradient
971    /// term behind Ipopt's `IpRestoConvCheck.cpp` `LOCALLY_INFEASIBLE`
972    /// test, applied here in the main loop.
973    pub fn curr_infeasibility_stationarity(&self) -> Number {
974        let c = self.curr_c();
975        let dms = self.curr_d_minus_s();
976        let jc_t_c = self.curr_jac_c_t_times_vec(&*c);
977        let jd_t_dms = self.curr_jac_d_t_times_vec(&*dms);
978        let mut grad = jc_t_c.make_new();
979        grad.add_two_vectors(1.0, &*jc_t_c, 1.0, &*jd_t_dms, 0.0);
980        let viol = c.amax().max(dms.amax());
981        grad.amax() / viol.max(1.0)
982    }
983
984    // --------------------------------------------------------------
985    // Average / scalar complementarity
986    // --------------------------------------------------------------
987
988    /// `(z_L · s_L + z_U · s_U + v_L · s_L^d + v_U · s_U^d) / N`
989    /// where `N` is the total number of bound multipliers
990    /// (`IpIpoptCalculatedQuantities.cpp:3553-3606`).
991    pub fn curr_avrg_compl(&self) -> Number {
992        let iv = self.curr_iv();
993        let n = iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
994        if n == 0 {
995            return 0.0;
996        }
997        let s_x_l = self.curr_slack_x_l();
998        let s_x_u = self.curr_slack_x_u();
999        let s_s_l = self.curr_slack_s_l();
1000        let s_s_u = self.curr_slack_s_u();
1001        let mut acc = iv.z_l.dot(&*s_x_l);
1002        acc += iv.z_u.dot(&*s_x_u);
1003        acc += iv.v_l.dot(&*s_s_l);
1004        acc += iv.v_u.dot(&*s_s_u);
1005        acc / Number::from(n)
1006    }
1007
1008    /// `min_i (s_i · z_i)` over all four bound complementarity blocks.
1009    /// Mirrors `IpIpoptCalculatedQuantities.cpp:CurrComplxMin`
1010    /// (lines 3608-3640) — the smallest pairwise product `s · z`,
1011    /// signalling how close the iterate is to the central path.
1012    /// Empty bound sets contribute `+∞`; returns `0` if no bounds at
1013    /// all.
1014    pub fn curr_complementarity_min(&self) -> Number {
1015        let cxl = self.curr_compl_x_l();
1016        let cxu = self.curr_compl_x_u();
1017        let csl = self.curr_compl_s_l();
1018        let csu = self.curr_compl_s_u();
1019        let m = |v: &Rc<dyn Vector>| {
1020            if v.dim() == 0 {
1021                Number::INFINITY
1022            } else {
1023                v.min()
1024            }
1025        };
1026        let acc = m(&cxl).min(m(&cxu)).min(m(&csl)).min(m(&csu));
1027        if acc.is_infinite() {
1028            0.0
1029        } else {
1030            acc
1031        }
1032    }
1033
1034    /// Max-norm of the unbarriered complementarity blocks
1035    /// `max_i |s_i · z_i|` across all four `(x_L, x_U, s_L, s_U)`
1036    /// pairs. Mirrors upstream
1037    /// `IpIpoptCalculatedQuantities.cpp:CurrComplementarity(0., NORM_MAX)`
1038    /// — used by `OptimalityErrorConvergenceCheck` to gate the
1039    /// per-component `compl_inf_tol` test independently of the scaled
1040    /// scalar `curr_nlp_error`.
1041    pub fn curr_complementarity_max(&self) -> Number {
1042        self.curr_compl_x_l()
1043            .amax()
1044            .max(self.curr_compl_x_u().amax())
1045            .max(self.curr_compl_s_l().amax())
1046            .max(self.curr_compl_s_u().amax())
1047    }
1048
1049    /// Centrality measure `ξ = min_i(s_i z_i) / avrg(s · z)`. Mirrors
1050    /// `IpIpoptCalculatedQuantities.cpp:CurrCentralityMeasure`. Used
1051    /// by [`crate::mu::oracle::loqo::LoqoMuOracle`] to bias σ toward
1052    /// the central path when the iterate is unbalanced. Returns `1.0`
1053    /// (perfectly central) when there are no bound multipliers.
1054    pub fn curr_centrality_measure(&self) -> Number {
1055        let avrg = self.curr_avrg_compl();
1056        if avrg <= 0.0 {
1057            return 1.0;
1058        }
1059        self.curr_complementarity_min() / avrg
1060    }
1061
1062    /// Barriered KKT error `E_μ(x,y,z)` — port of
1063    /// `IpIpoptCalculatedQuantities.cpp:CurrBarrierError`. Same as
1064    /// [`Self::curr_nlp_error`] but uses the *relaxed* complementarity
1065    /// `s ⊙ z − μ` so the residual is zero when the iterate sits on the
1066    /// μ-perturbed central path. The monotone barrier-update strategy
1067    /// reduces μ only once this error drops below
1068    /// `barrier_tol_factor · μ`.
1069    pub fn curr_barrier_error(&self) -> Number {
1070        let iv = self.curr_iv();
1071        let (s_d, s_c) = self.optimality_error_scaling(&iv);
1072
1073        let glx = self.curr_grad_lag_x();
1074        let gls = self.curr_grad_lag_s();
1075        let dual = glx.amax().max(gls.amax()) / s_d;
1076
1077        let c = self.curr_c();
1078        let dms = self.curr_d_minus_s();
1079        let primal = c.amax().max(dms.amax());
1080
1081        let compl = self
1082            .curr_relaxed_compl_x_l()
1083            .amax()
1084            .max(self.curr_relaxed_compl_x_u().amax())
1085            .max(self.curr_relaxed_compl_s_l().amax())
1086            .max(self.curr_relaxed_compl_s_u().amax())
1087            / s_c;
1088
1089        dual.max(primal).max(compl)
1090    }
1091
1092    /// Optimality-scaled max-norm KKT error — port of
1093    /// `IpIpoptCalculatedQuantities.cpp:3050-3104`.
1094    ///
1095    /// ```text
1096    ///   E = max( ||∇_x L, ∇_s L||_∞ / s_d ,
1097    ///            ||c, d − s||_∞ ,
1098    ///            ||compl||_∞ / s_c )
1099    /// ```
1100    ///
1101    /// where `s_d` / `s_c` are the asum-based scalings from
1102    /// `ComputeOptimalityErrorScaling` (see §4 of `MAIN_LOOP.md`).
1103    /// Uses `mu_target = 0` (the unbarriered KKT residual). The
1104    /// barriered variant is `curr_barrier_error` (TODO in Phase 7).
1105    pub fn curr_nlp_error(&self) -> Number {
1106        let iv = self.curr_iv();
1107        let (s_d, s_c) = self.optimality_error_scaling(&iv);
1108
1109        // dual infeasibility (max-norm of grad_lag_x and grad_lag_s)
1110        let glx = self.curr_grad_lag_x();
1111        let gls = self.curr_grad_lag_s();
1112        let dual = glx.amax().max(gls.amax()) / s_d;
1113
1114        // primal: max(||c||, ||d-s||)
1115        let c = self.curr_c();
1116        let dms = self.curr_d_minus_s();
1117        let primal = c.amax().max(dms.amax());
1118
1119        // unbarriered complementarity (mu_target = 0 → just ||compl||)
1120        let compl = self
1121            .curr_compl_x_l()
1122            .amax()
1123            .max(self.curr_compl_x_u().amax())
1124            .max(self.curr_compl_s_l().amax())
1125            .max(self.curr_compl_s_u().amax())
1126            / s_c;
1127
1128        dual.max(primal).max(compl)
1129    }
1130
1131    /// `(s_d, s_c)` per `ComputeOptimalityErrorScaling`
1132    /// (`IpIpoptCalculatedQuantities.cpp:3663-3700`).
1133    fn optimality_error_scaling(&self, iv: &IteratesVector) -> (Number, Number) {
1134        let s_max = self.s_max;
1135
1136        // s_c: mean asum of all bound multipliers, capped at s_max,
1137        //      divided by s_max.
1138        let n_c = iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
1139        let s_c = if n_c == 0 {
1140            1.0
1141        } else {
1142            let asum = iv.z_l.asum() + iv.z_u.asum() + iv.v_l.asum() + iv.v_u.asum();
1143            (s_max.max(asum / Number::from(n_c))) / s_max
1144        };
1145
1146        // s_d: mean asum of all dual multipliers, capped, divided.
1147        let n_d =
1148            iv.y_c.dim() + iv.y_d.dim() + iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
1149        let s_d = if n_d == 0 {
1150            1.0
1151        } else {
1152            let asum = iv.y_c.asum()
1153                + iv.y_d.asum()
1154                + iv.z_l.asum()
1155                + iv.z_u.asum()
1156                + iv.v_l.asum()
1157                + iv.v_u.asum();
1158            (s_max.max(asum / Number::from(n_d))) / s_max
1159        };
1160
1161        (s_d, s_c)
1162    }
1163
1164    // --------------------------------------------------------------
1165    // Trial-side Lagrangian gradient / complementarity — needed by
1166    // the soft restoration phase's primal-dual error test. Each is a
1167    // line-for-line analog of the `curr_*` method above, reading the
1168    // `trial` iterate instead of `curr`.
1169    // --------------------------------------------------------------
1170
1171    pub fn trial_jac_c(&self) -> Rc<dyn Matrix> {
1172        let iv = self.trial_iv();
1173        self.nlp.borrow_mut().eval_jac_c(&*iv.x)
1174    }
1175
1176    pub fn trial_jac_d(&self) -> Rc<dyn Matrix> {
1177        let iv = self.trial_iv();
1178        self.nlp.borrow_mut().eval_jac_d(&*iv.x)
1179    }
1180
1181    /// `∇_x L` at the trial iterate — analog of [`Self::curr_grad_lag_x`].
1182    pub fn trial_grad_lag_x(&self) -> Rc<dyn Vector> {
1183        let iv = self.trial_iv();
1184        let grad_f = self.trial_grad_f();
1185        let jac_c = self.trial_jac_c();
1186        let jac_d = self.trial_jac_d();
1187
1188        let mut jc_t = iv.x.make_new();
1189        jac_c.trans_mult_vector(1.0, &*iv.y_c, 0.0, &mut *jc_t);
1190        let mut jd_t = iv.x.make_new();
1191        jac_d.trans_mult_vector(1.0, &*iv.y_d, 0.0, &mut *jd_t);
1192
1193        let mut tmp = iv.x.make_new();
1194        tmp.copy(&*grad_f);
1195        tmp.add_two_vectors(1.0, &*jc_t, 1.0, &*jd_t, 1.0);
1196
1197        let nlp = self.nlp.borrow();
1198        nlp.px_l().mult_vector(-1.0, &*iv.z_l, 1.0, &mut *tmp);
1199        nlp.px_u().mult_vector(1.0, &*iv.z_u, 1.0, &mut *tmp);
1200        rc_from(tmp)
1201    }
1202
1203    /// `∇_s L` at the trial iterate — analog of [`Self::curr_grad_lag_s`].
1204    pub fn trial_grad_lag_s(&self) -> Rc<dyn Vector> {
1205        let iv = self.trial_iv();
1206        let mut tmp = iv.y_d.make_new();
1207        let nlp = self.nlp.borrow();
1208        nlp.pd_u().mult_vector(1.0, &*iv.v_u, 0.0, &mut *tmp);
1209        nlp.pd_l().mult_vector(-1.0, &*iv.v_l, 1.0, &mut *tmp);
1210        tmp.axpy(-1.0, &*iv.y_d);
1211        rc_from(tmp)
1212    }
1213
1214    pub fn trial_compl_x_l(&self) -> Rc<dyn Vector> {
1215        Self::calc_compl(&*self.trial_slack_x_l(), &*self.trial_iv().z_l)
1216    }
1217
1218    pub fn trial_compl_x_u(&self) -> Rc<dyn Vector> {
1219        Self::calc_compl(&*self.trial_slack_x_u(), &*self.trial_iv().z_u)
1220    }
1221
1222    pub fn trial_compl_s_l(&self) -> Rc<dyn Vector> {
1223        Self::calc_compl(&*self.trial_slack_s_l(), &*self.trial_iv().v_l)
1224    }
1225
1226    pub fn trial_compl_s_u(&self) -> Rc<dyn Vector> {
1227        Self::calc_compl(&*self.trial_slack_s_u(), &*self.trial_iv().v_u)
1228    }
1229
1230    /// `||s ⊙ z − μ||₁` summed over the four complementarity blocks.
1231    fn relaxed_compl_asum(blocks: &[Rc<dyn Vector>], mu: Number) -> Number {
1232        let mut acc = 0.0;
1233        for compl in blocks {
1234            if compl.dim() == 0 {
1235                continue;
1236            }
1237            let mut r = compl.make_new();
1238            r.copy(&**compl);
1239            r.add_scalar(-mu);
1240            acc += r.asum();
1241        }
1242        acc
1243    }
1244
1245    /// Unscaled primal-dual KKT system error at the current iterate —
1246    /// port of
1247    /// `IpIpoptCalculatedQuantities.cpp:curr_primal_dual_system_error`.
1248    /// Each block uses the 1-norm scaled by its entry count; the result
1249    /// is the sum of the dual-infeasibility, primal-infeasibility, and
1250    /// complementarity terms. Used by the soft restoration phase's
1251    /// sufficient-reduction test.
1252    pub fn curr_primal_dual_system_error(&self, mu: Number) -> Number {
1253        let iv = self.curr_iv();
1254        let n_dual = iv.x.dim() + iv.s.dim();
1255        let dual_inf =
1256            (self.curr_grad_lag_x().asum() + self.curr_grad_lag_s().asum()) / Number::from(n_dual);
1257
1258        let n_primal = iv.y_c.dim() + iv.y_d.dim();
1259        let primal_inf = if n_primal > 0 {
1260            (self.curr_c().asum() + self.curr_d_minus_s().asum()) / Number::from(n_primal)
1261        } else {
1262            0.0
1263        };
1264
1265        let n_cmpl = iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
1266        let cmpl = if n_cmpl > 0 {
1267            Self::relaxed_compl_asum(
1268                &[
1269                    self.curr_compl_x_l(),
1270                    self.curr_compl_x_u(),
1271                    self.curr_compl_s_l(),
1272                    self.curr_compl_s_u(),
1273                ],
1274                mu,
1275            ) / Number::from(n_cmpl)
1276        } else {
1277            0.0
1278        };
1279
1280        dual_inf + primal_inf + cmpl
1281    }
1282
1283    /// Unscaled primal-dual KKT system error at the trial iterate —
1284    /// trial-side analog of [`Self::curr_primal_dual_system_error`].
1285    pub fn trial_primal_dual_system_error(&self, mu: Number) -> Number {
1286        let iv = self.trial_iv();
1287        let n_dual = iv.x.dim() + iv.s.dim();
1288        let dual_inf = (self.trial_grad_lag_x().asum() + self.trial_grad_lag_s().asum())
1289            / Number::from(n_dual);
1290
1291        let n_primal = iv.y_c.dim() + iv.y_d.dim();
1292        let primal_inf = if n_primal > 0 {
1293            (self.trial_c().asum() + self.trial_d_minus_s().asum()) / Number::from(n_primal)
1294        } else {
1295            0.0
1296        };
1297
1298        let n_cmpl = iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
1299        let cmpl = if n_cmpl > 0 {
1300            Self::relaxed_compl_asum(
1301                &[
1302                    self.trial_compl_x_l(),
1303                    self.trial_compl_x_u(),
1304                    self.trial_compl_s_l(),
1305                    self.trial_compl_s_u(),
1306                ],
1307                mu,
1308            ) / Number::from(n_cmpl)
1309        } else {
1310            0.0
1311        };
1312
1313        dual_inf + primal_inf + cmpl
1314    }
1315
1316    // --------------------------------------------------------------
1317    // Damping indicators — `IpIpoptCalculatedQuantities.cpp:1044-1092`.
1318    //
1319    //   Tmp_x = P_L · 1 − P_U · 1   (per primal: +1 lower-only,
1320    //                                 −1 upper-only, 0 two-sided,
1321    //                                 0 unbounded)
1322    //   dampind_x_L =  P_L^T · Tmp_x   (1 on lower-only bounds)
1323    //   dampind_x_U = −P_U^T · Tmp_x   (1 on upper-only bounds)
1324    // --------------------------------------------------------------
1325
1326    fn damping_indicators(&self) -> DampingIndicators {
1327        let nlp = self.nlp.borrow();
1328
1329        let mut tmp_x_l = nlp.x_l().make_new();
1330        tmp_x_l.set(1.0);
1331        let mut tmp_x_u = nlp.x_u().make_new();
1332        tmp_x_u.set(1.0);
1333        let mut tmp_x = self.curr_iv().x.make_new();
1334        nlp.px_l().mult_vector(1.0, &*tmp_x_l, 0.0, &mut *tmp_x);
1335        nlp.px_u().mult_vector(-1.0, &*tmp_x_u, 1.0, &mut *tmp_x);
1336        let mut d_x_l = nlp.x_l().make_new();
1337        nlp.px_l().trans_mult_vector(1.0, &*tmp_x, 0.0, &mut *d_x_l);
1338        let mut d_x_u = nlp.x_u().make_new();
1339        nlp.px_u()
1340            .trans_mult_vector(-1.0, &*tmp_x, 0.0, &mut *d_x_u);
1341
1342        let mut tmp_s_l = nlp.d_l().make_new();
1343        tmp_s_l.set(1.0);
1344        let mut tmp_s_u = nlp.d_u().make_new();
1345        tmp_s_u.set(1.0);
1346        let mut tmp_s = self.curr_iv().s.make_new();
1347        nlp.pd_l().mult_vector(1.0, &*tmp_s_l, 0.0, &mut *tmp_s);
1348        nlp.pd_u().mult_vector(-1.0, &*tmp_s_u, 1.0, &mut *tmp_s);
1349        let mut d_s_l = nlp.d_l().make_new();
1350        nlp.pd_l().trans_mult_vector(1.0, &*tmp_s, 0.0, &mut *d_s_l);
1351        let mut d_s_u = nlp.d_u().make_new();
1352        nlp.pd_u()
1353            .trans_mult_vector(-1.0, &*tmp_s, 0.0, &mut *d_s_u);
1354
1355        DampingIndicators {
1356            x_l: rc_from(d_x_l),
1357            x_u: rc_from(d_x_u),
1358            s_l: rc_from(d_s_l),
1359            s_u: rc_from(d_s_u),
1360        }
1361    }
1362
1363    /// `curr_grad_lag_x` plus the `kappa_d · μ · (Px_L · 1 − Px_U · 1)`
1364    /// damping term on singly-bounded primals — port of
1365    /// `IpIpoptCalculatedQuantities.cpp:2131-2180`. When `kappa_d == 0`
1366    /// returns the un-damped gradient.
1367    pub fn curr_grad_lag_with_damping_x(&self) -> Rc<dyn Vector> {
1368        if self.kappa_d == 0.0 {
1369            return self.curr_grad_lag_x();
1370        }
1371        let mu = self.data.borrow().curr_mu;
1372        let di = self.damping_indicators();
1373        let (d_x_l, d_x_u) = (di.x_l, di.x_u);
1374        let glx = self.curr_grad_lag_x();
1375        let mut tmp = glx.make_new();
1376        tmp.copy(&*glx);
1377        let nlp = self.nlp.borrow();
1378        nlp.px_l()
1379            .mult_vector(self.kappa_d * mu, &*d_x_l, 1.0, &mut *tmp);
1380        nlp.px_u()
1381            .mult_vector(-self.kappa_d * mu, &*d_x_u, 1.0, &mut *tmp);
1382        rc_from(tmp)
1383    }
1384
1385    pub fn curr_grad_lag_with_damping_s(&self) -> Rc<dyn Vector> {
1386        if self.kappa_d == 0.0 {
1387            return self.curr_grad_lag_s();
1388        }
1389        let mu = self.data.borrow().curr_mu;
1390        let di = self.damping_indicators();
1391        let (d_s_l, d_s_u) = (di.s_l, di.s_u);
1392        let gls = self.curr_grad_lag_s();
1393        let mut tmp = gls.make_new();
1394        tmp.copy(&*gls);
1395        let nlp = self.nlp.borrow();
1396        nlp.pd_l()
1397            .mult_vector(self.kappa_d * mu, &*d_s_l, 1.0, &mut *tmp);
1398        nlp.pd_u()
1399            .mult_vector(-self.kappa_d * mu, &*d_s_u, 1.0, &mut *tmp);
1400        rc_from(tmp)
1401    }
1402
1403    /// `kappa_d · (P_L · damping_l − P_U · damping_u)` in the full x
1404    /// space — port of `IpIpoptCalculatedQuantities.cpp::grad_kappa_times_damping_x`
1405    /// (lines 912-949). Unlike `curr_grad_lag_with_damping_x` this does
1406    /// NOT include `grad_lag_x` and is NOT scaled by `mu`; the centering
1407    /// RHS in the quality-function oracle multiplies the returned vector
1408    /// by `-avrg_compl` per upstream `IpQualityFunctionMuOracle.cpp:229`.
1409    pub fn grad_kappa_times_damping_x(&self) -> Rc<dyn Vector> {
1410        let mut tmp = self.curr_iv().x.make_new();
1411        tmp.set(0.0);
1412        if self.kappa_d > 0.0 {
1413            let di = self.damping_indicators();
1414            let nlp = self.nlp.borrow();
1415            nlp.px_l()
1416                .mult_vector(self.kappa_d, &*di.x_l, 0.0, &mut *tmp);
1417            nlp.px_u()
1418                .mult_vector(-self.kappa_d, &*di.x_u, 1.0, &mut *tmp);
1419        }
1420        rc_from(tmp)
1421    }
1422
1423    pub fn grad_kappa_times_damping_s(&self) -> Rc<dyn Vector> {
1424        let mut tmp = self.curr_iv().s.make_new();
1425        tmp.set(0.0);
1426        if self.kappa_d > 0.0 {
1427            let di = self.damping_indicators();
1428            let nlp = self.nlp.borrow();
1429            nlp.pd_l()
1430                .mult_vector(self.kappa_d, &*di.s_l, 0.0, &mut *tmp);
1431            nlp.pd_u()
1432                .mult_vector(-self.kappa_d, &*di.s_u, 1.0, &mut *tmp);
1433        }
1434        rc_from(tmp)
1435    }
1436
1437    // --------------------------------------------------------------
1438    // Affine (predictor) step helpers — port of upstream
1439    // `IpIpoptCalculatedQuantities.cpp:CurrAvrgCompl`/`AffMaxAlpha…`
1440    // used by the Mehrotra probing oracle and the quality-function
1441    // oracle's σ-search.
1442    // --------------------------------------------------------------
1443
1444    /// Max primal step that keeps `s + α · Δs > 0` for the four slack
1445    /// blocks (x_L, x_U, s_L, s_U), bounded by the fraction-to-the-
1446    /// boundary parameter `τ ∈ (0, 1]`. Mirrors
1447    /// `CalcFracToBound` against the projected step `P_L^T Δx`,
1448    /// `−P_U^T Δx`, `P_L^T Δs`, `−P_U^T Δs`.
1449    pub fn aff_step_alpha_primal_max(&self, delta_aff: &IteratesVector, tau: Number) -> Number {
1450        let nlp = self.nlp.borrow();
1451        let s_x_l = self.curr_slack_x_l();
1452        let s_x_u = self.curr_slack_x_u();
1453        let s_s_l = self.curr_slack_s_l();
1454        let s_s_u = self.curr_slack_s_u();
1455
1456        // Project Δx / Δs onto each bound subspace with the right sign.
1457        let mut step_x_l = s_x_l.make_new();
1458        nlp.px_l()
1459            .trans_mult_vector(1.0, &*delta_aff.x, 0.0, &mut *step_x_l);
1460        let mut step_x_u = s_x_u.make_new();
1461        nlp.px_u()
1462            .trans_mult_vector(-1.0, &*delta_aff.x, 0.0, &mut *step_x_u);
1463        let mut step_s_l = s_s_l.make_new();
1464        nlp.pd_l()
1465            .trans_mult_vector(1.0, &*delta_aff.s, 0.0, &mut *step_s_l);
1466        let mut step_s_u = s_s_u.make_new();
1467        nlp.pd_u()
1468            .trans_mult_vector(-1.0, &*delta_aff.s, 0.0, &mut *step_s_u);
1469
1470        s_x_l
1471            .frac_to_bound(&*step_x_l, tau)
1472            .min(s_x_u.frac_to_bound(&*step_x_u, tau))
1473            .min(s_s_l.frac_to_bound(&*step_s_l, tau))
1474            .min(s_s_u.frac_to_bound(&*step_s_u, tau))
1475    }
1476
1477    /// Max dual step that keeps `z + α · Δz > 0` (and same for v).
1478    pub fn aff_step_alpha_dual_max(&self, delta_aff: &IteratesVector, tau: Number) -> Number {
1479        let iv = self.curr_iv();
1480        iv.z_l
1481            .frac_to_bound(&*delta_aff.z_l, tau)
1482            .min(iv.z_u.frac_to_bound(&*delta_aff.z_u, tau))
1483            .min(iv.v_l.frac_to_bound(&*delta_aff.v_l, tau))
1484            .min(iv.v_u.frac_to_bound(&*delta_aff.v_u, tau))
1485    }
1486
1487    /// Predicted average complementarity after the affine step:
1488    /// `(1/N) · Σ (s + α_pri · Δs) · (z + α_du · Δz)` summed over the
1489    /// four bound blocks. Returns `0` when there are no bounds.
1490    pub fn aff_step_compl_avrg(
1491        &self,
1492        delta_aff: &IteratesVector,
1493        alpha_primal: Number,
1494        alpha_dual: Number,
1495    ) -> Number {
1496        let iv = self.curr_iv();
1497        let n = iv.z_l.dim() + iv.z_u.dim() + iv.v_l.dim() + iv.v_u.dim();
1498        if n == 0 {
1499            return 0.0;
1500        }
1501        let nlp = self.nlp.borrow();
1502
1503        // s_X_L_aff = s_X_L + α_pri · P_L^T Δx
1504        let s_x_l = self.curr_slack_x_l();
1505        let mut s_x_l_aff = s_x_l.make_new();
1506        s_x_l_aff.copy(&*s_x_l);
1507        let mut tmp = s_x_l.make_new();
1508        nlp.px_l()
1509            .trans_mult_vector(1.0, &*delta_aff.x, 0.0, &mut *tmp);
1510        s_x_l_aff.axpy(alpha_primal, &*tmp);
1511        // z_L_aff = z_L + α_du · Δz_L
1512        let mut z_l_aff = iv.z_l.make_new();
1513        z_l_aff.copy(&*iv.z_l);
1514        z_l_aff.axpy(alpha_dual, &*delta_aff.z_l);
1515        let mut acc = s_x_l_aff.dot(&*z_l_aff);
1516
1517        // s_X_U_aff = s_X_U − α_pri · P_U^T Δx
1518        let s_x_u = self.curr_slack_x_u();
1519        let mut s_x_u_aff = s_x_u.make_new();
1520        s_x_u_aff.copy(&*s_x_u);
1521        let mut tmp = s_x_u.make_new();
1522        nlp.px_u()
1523            .trans_mult_vector(-1.0, &*delta_aff.x, 0.0, &mut *tmp);
1524        s_x_u_aff.axpy(alpha_primal, &*tmp);
1525        let mut z_u_aff = iv.z_u.make_new();
1526        z_u_aff.copy(&*iv.z_u);
1527        z_u_aff.axpy(alpha_dual, &*delta_aff.z_u);
1528        acc += s_x_u_aff.dot(&*z_u_aff);
1529
1530        // s_S_L_aff = s_S_L + α_pri · P_dL^T Δs
1531        let s_s_l = self.curr_slack_s_l();
1532        let mut s_s_l_aff = s_s_l.make_new();
1533        s_s_l_aff.copy(&*s_s_l);
1534        let mut tmp = s_s_l.make_new();
1535        nlp.pd_l()
1536            .trans_mult_vector(1.0, &*delta_aff.s, 0.0, &mut *tmp);
1537        s_s_l_aff.axpy(alpha_primal, &*tmp);
1538        let mut v_l_aff = iv.v_l.make_new();
1539        v_l_aff.copy(&*iv.v_l);
1540        v_l_aff.axpy(alpha_dual, &*delta_aff.v_l);
1541        acc += s_s_l_aff.dot(&*v_l_aff);
1542
1543        // s_S_U_aff = s_S_U − α_pri · P_dU^T Δs
1544        let s_s_u = self.curr_slack_s_u();
1545        let mut s_s_u_aff = s_s_u.make_new();
1546        s_s_u_aff.copy(&*s_s_u);
1547        let mut tmp = s_s_u.make_new();
1548        nlp.pd_u()
1549            .trans_mult_vector(-1.0, &*delta_aff.s, 0.0, &mut *tmp);
1550        s_s_u_aff.axpy(alpha_primal, &*tmp);
1551        let mut v_u_aff = iv.v_u.make_new();
1552        v_u_aff.copy(&*iv.v_u);
1553        v_u_aff.axpy(alpha_dual, &*delta_aff.v_u);
1554        acc += s_s_u_aff.dot(&*v_u_aff);
1555
1556        acc / Number::from(n)
1557    }
1558}
1559
1560/// Convenience handle. Mirrors upstream's `SmartPtr<CQ>` flow.
1561pub type IpoptCqHandle = Rc<RefCell<IpoptCalculatedQuantities>>;
1562
1563/// Bundle of damping indicators for the four bound spaces — kept
1564/// internal because `kappa_d == 0` makes them dead in the default
1565/// configuration.
1566struct DampingIndicators {
1567    x_l: Rc<dyn Vector>,
1568    x_u: Rc<dyn Vector>,
1569    s_l: Rc<dyn Vector>,
1570    s_u: Rc<dyn Vector>,
1571}
1572
1573#[cfg(test)]
1574mod tests {
1575    use super::*;
1576    use crate::ipopt_data::IpoptData;
1577    use crate::iterates_vector::IteratesVector;
1578    use pounce_common::types::Index;
1579    use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
1580    use pounce_linalg::expansion_matrix::{ExpansionMatrix, ExpansionMatrixSpace};
1581    use std::rc::Rc as StdRc;
1582
1583    fn dvec(values: &[Number]) -> DenseVector {
1584        let space = DenseVectorSpace::new(values.len() as Index);
1585        let mut v = space.make_new_dense();
1586        v.values_mut().copy_from_slice(values);
1587        v
1588    }
1589
1590    fn rcv(values: &[Number]) -> Rc<dyn Vector> {
1591        StdRc::new(dvec(values))
1592    }
1593
1594    /// Mock IpoptNlp covering: 2 vars, 1 equality, 1 inequality.
1595    /// Bounds: x[0] ≥ 0, x[1] ≤ 5, d ≥ 1.
1596    /// f(x) = x[0]^2 + x[1]^2; ∇f = (2x[0], 2x[1])
1597    /// c(x) = x[0] + x[1] - 1
1598    /// d(x) = x[0]
1599    struct MockNlp {
1600        x_l: DenseVector,
1601        x_u: DenseVector,
1602        d_l: DenseVector,
1603        d_u: DenseVector,
1604        px_l: Rc<dyn Matrix>,
1605        px_u: Rc<dyn Matrix>,
1606        pd_l: Rc<dyn Matrix>,
1607        pd_u: Rc<dyn Matrix>,
1608    }
1609
1610    impl MockNlp {
1611        fn new() -> Self {
1612            // x_L holds finite lower bounds; here only x[0] has one (=0).
1613            let x_l = dvec(&[0.0]);
1614            // x_U holds finite upper bounds; here only x[1] has one (=5).
1615            let x_u = dvec(&[5.0]);
1616            // d has one finite lower bound (d ≥ 1) and no finite upper.
1617            let d_l = dvec(&[1.0]);
1618            let d_u = dvec(&[]);
1619
1620            let px_l_space = ExpansionMatrixSpace::new(2, 1, &[0], 0);
1621            let px_u_space = ExpansionMatrixSpace::new(2, 1, &[1], 0);
1622            let pd_l_space = ExpansionMatrixSpace::new(1, 1, &[0], 0);
1623            let pd_u_space = ExpansionMatrixSpace::new(1, 0, &[], 0);
1624
1625            Self {
1626                x_l,
1627                x_u,
1628                d_l,
1629                d_u,
1630                px_l: StdRc::new(ExpansionMatrix::new(px_l_space)),
1631                px_u: StdRc::new(ExpansionMatrix::new(px_u_space)),
1632                pd_l: StdRc::new(ExpansionMatrix::new(pd_l_space)),
1633                pd_u: StdRc::new(ExpansionMatrix::new(pd_u_space)),
1634            }
1635        }
1636    }
1637
1638    impl crate::ipopt_nlp::Nlp for MockNlp {
1639        fn n(&self) -> Index {
1640            2
1641        }
1642        fn m_eq(&self) -> Index {
1643            1
1644        }
1645        fn m_ineq(&self) -> Index {
1646            1
1647        }
1648        fn eval_f(&mut self, x: &dyn Vector) -> Number {
1649            // f(x) = x[0]^2 + x[1]^2
1650            let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
1651            xx.values()[0] * xx.values()[0] + xx.values()[1] * xx.values()[1]
1652        }
1653        fn eval_grad_f(&mut self, x: &dyn Vector, g: &mut dyn Vector) {
1654            // grad f = (2 x[0], 2 x[1])
1655            let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
1656            let gg = g.as_any_mut().downcast_mut::<DenseVector>().unwrap();
1657            gg.values_mut()[0] = 2.0 * xx.values()[0];
1658            gg.values_mut()[1] = 2.0 * xx.values()[1];
1659        }
1660        fn eval_c(&mut self, x: &dyn Vector, c: &mut dyn Vector) {
1661            let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
1662            let cc = c.as_any_mut().downcast_mut::<DenseVector>().unwrap();
1663            cc.values_mut()[0] = xx.values()[0] + xx.values()[1] - 1.0;
1664        }
1665        fn eval_d(&mut self, x: &dyn Vector, d: &mut dyn Vector) {
1666            let xx = x.as_any().downcast_ref::<DenseVector>().unwrap();
1667            let dd = d.as_any_mut().downcast_mut::<DenseVector>().unwrap();
1668            dd.values_mut()[0] = xx.values()[0];
1669        }
1670        fn eval_jac_c(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
1671            unimplemented!("not exercised in Phase 5 unit tests")
1672        }
1673        fn eval_jac_d(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
1674            unimplemented!("not exercised in Phase 5 unit tests")
1675        }
1676        fn eval_h(
1677            &mut self,
1678            _x: &dyn Vector,
1679            _obj_factor: Number,
1680            _y_c: &dyn Vector,
1681            _y_d: &dyn Vector,
1682        ) -> Rc<dyn SymMatrix> {
1683            unimplemented!()
1684        }
1685    }
1686
1687    impl IpoptNlp for MockNlp {
1688        fn x_l(&self) -> &dyn Vector {
1689            &self.x_l
1690        }
1691        fn x_u(&self) -> &dyn Vector {
1692            &self.x_u
1693        }
1694        fn d_l(&self) -> &dyn Vector {
1695            &self.d_l
1696        }
1697        fn d_u(&self) -> &dyn Vector {
1698            &self.d_u
1699        }
1700        fn px_l(&self) -> Rc<dyn Matrix> {
1701            self.px_l.clone()
1702        }
1703        fn px_u(&self) -> Rc<dyn Matrix> {
1704            self.px_u.clone()
1705        }
1706        fn pd_l(&self) -> Rc<dyn Matrix> {
1707            self.pd_l.clone()
1708        }
1709        fn pd_u(&self) -> Rc<dyn Matrix> {
1710            self.pd_u.clone()
1711        }
1712    }
1713
1714    fn fixture() -> IpoptCalculatedQuantities {
1715        let mut data = IpoptData::new();
1716        data.curr_mu = 0.1;
1717        // Iterate: x = (2, 3); s = (4); y_c = (1); y_d = (1);
1718        // z_L = (0.5) [bound on x[0]], z_U = (0.7) [bound on x[1]],
1719        // v_L = (0.3), v_U = ().
1720        let iv = IteratesVector::new(
1721            rcv(&[2.0, 3.0]),
1722            rcv(&[4.0]),
1723            rcv(&[1.0]),
1724            rcv(&[1.0]),
1725            rcv(&[0.5]),
1726            rcv(&[0.7]),
1727            rcv(&[0.3]),
1728            rcv(&[]),
1729        );
1730        data.set_curr(iv);
1731        let data_handle = StdRc::new(RefCell::new(data));
1732        let nlp: StdRc<RefCell<dyn IpoptNlp>> = StdRc::new(RefCell::new(MockNlp::new()));
1733        let mut cq = IpoptCalculatedQuantities::new(data_handle, nlp);
1734        // Disable damping for clean unit-test expectations.
1735        cq.kappa_d = 0.0;
1736        cq
1737    }
1738
1739    fn dense_vals(v: &Rc<dyn Vector>) -> Vec<Number> {
1740        v.as_any()
1741            .downcast_ref::<DenseVector>()
1742            .unwrap()
1743            .values()
1744            .to_vec()
1745    }
1746
1747    #[test]
1748    fn slack_x_lower_is_x0_minus_x_l() {
1749        // P_L^T x = [x[0]] = [2]; x_L = [0]; slack = 2 - 0 = 2.
1750        let cq = fixture();
1751        assert_eq!(dense_vals(&cq.curr_slack_x_l()), vec![2.0]);
1752    }
1753
1754    #[test]
1755    fn slack_x_upper_is_x_u_minus_x1() {
1756        // x_U = [5]; P_U^T x = [3]; slack = 5 - 3 = 2.
1757        let cq = fixture();
1758        assert_eq!(dense_vals(&cq.curr_slack_x_u()), vec![2.0]);
1759    }
1760
1761    #[test]
1762    fn slack_s_lower() {
1763        // d_L = [1]; P_L^T s = [4]; slack = 4 - 1 = 3.
1764        let cq = fixture();
1765        assert_eq!(dense_vals(&cq.curr_slack_s_l()), vec![3.0]);
1766    }
1767
1768    #[test]
1769    fn grad_f_is_twice_x() {
1770        let cq = fixture();
1771        assert_eq!(dense_vals(&cq.curr_grad_f()), vec![4.0, 6.0]);
1772    }
1773
1774    #[test]
1775    fn compl_x_l_is_slack_times_z() {
1776        // slack_x_L = [2]; z_L = [0.5]; compl = [1.0]
1777        let cq = fixture();
1778        assert_eq!(dense_vals(&cq.curr_compl_x_l()), vec![1.0]);
1779    }
1780
1781    #[test]
1782    fn relaxed_compl_x_l_subtracts_mu() {
1783        // compl = 1.0; mu = 0.1; relaxed = 0.9.
1784        let cq = fixture();
1785        assert!((dense_vals(&cq.curr_relaxed_compl_x_l())[0] - 0.9).abs() < 1e-15);
1786    }
1787
1788    #[test]
1789    fn sigma_x_routes_z_over_slack_through_p() {
1790        // P_L lifts (z_L/s_L) = (0.5/2 = 0.25) into x[0] slot.
1791        // P_U lifts (z_U/s_U) = (0.7/2 = 0.35) into x[1] slot.
1792        // sigma = (0.25, 0.35)
1793        let cq = fixture();
1794        let s = dense_vals(&cq.curr_sigma_x());
1795        assert!((s[0] - 0.25).abs() < 1e-15);
1796        assert!((s[1] - 0.35).abs() < 1e-15);
1797    }
1798
1799    #[test]
1800    fn sigma_s_lower_only() {
1801        // P_L lifts (v_L/s_L) = (0.3/3 = 0.1).
1802        let cq = fixture();
1803        let s = dense_vals(&cq.curr_sigma_s());
1804        assert!((s[0] - 0.1).abs() < 1e-15);
1805    }
1806
1807    #[test]
1808    fn avrg_compl_averages_over_active_bounds() {
1809        // z_L·s_L + z_U·s_U + v_L·s_s_L + v_U·s_s_U
1810        // = 0.5*2 + 0.7*2 + 0.3*3 + 0
1811        // = 1 + 1.4 + 0.9 = 3.3
1812        // N = 1 + 1 + 1 + 0 = 3 → 1.1
1813        let cq = fixture();
1814        assert!((cq.curr_avrg_compl() - 1.1).abs() < 1e-15);
1815    }
1816
1817    #[test]
1818    fn complementarity_min_takes_min_over_active_pairs() {
1819        // compl entries: z_L·s_L=1.0, z_U·s_U=1.4, v_L·s_s_L=0.9.
1820        // v_U is empty (skipped). Min = 0.9.
1821        let cq = fixture();
1822        assert!((cq.curr_complementarity_min() - 0.9).abs() < 1e-15);
1823    }
1824
1825    #[test]
1826    fn centrality_measure_is_min_over_avrg() {
1827        // min/avrg = 0.9 / 1.1 ≈ 0.81818…
1828        let cq = fixture();
1829        let xi = cq.curr_centrality_measure();
1830        assert!((xi - 0.9 / 1.1).abs() < 1e-15);
1831    }
1832
1833    #[test]
1834    fn curr_f_evaluates_objective() {
1835        // f(x) = x[0]^2 + x[1]^2 at x = (2, 3) → 4 + 9 = 13.
1836        let cq = fixture();
1837        assert!((cq.curr_f() - 13.0).abs() < 1e-15);
1838    }
1839
1840    #[test]
1841    fn curr_barrier_obj_subtracts_mu_log_slacks() {
1842        // f = 13; slacks = (s_x_L=2, s_x_U=2, s_s_L=3, s_s_U=∅).
1843        // log_sum = ln 2 + ln 2 + ln 3 + 0 = 2 ln 2 + ln 3.
1844        // mu = 0.1 → phi = 13 - 0.1*(2 ln 2 + ln 3).
1845        let cq = fixture();
1846        let expected = 13.0 - 0.1 * (2.0 * 2.0_f64.ln() + 3.0_f64.ln());
1847        assert!((cq.curr_barrier_obj() - expected).abs() < 1e-13);
1848    }
1849
1850    #[test]
1851    fn curr_constraint_violation_is_one_norm() {
1852        // c(x) = x[0]+x[1]-1 = 4 ⇒ |c| = 4.
1853        // d(x)=x[0]=2; s=4 ⇒ d-s = -2 ⇒ |d-s| = 2.
1854        // theta = 4 + 2 = 6.
1855        let cq = fixture();
1856        assert!((cq.curr_constraint_violation() - 6.0).abs() < 1e-13);
1857    }
1858
1859    #[test]
1860    fn grad_barrier_obj_x_subtracts_mu_inv_slack() {
1861        // grad_f = (4, 6).
1862        // P_L lifts -mu*(1/s_x_L) = -0.1*(1/2)=-0.05 into x[0].
1863        // P_U lifts +mu*(1/s_x_U) = +0.1*(1/2)=+0.05 into x[1].
1864        // result = (4 - 0.05, 6 + 0.05) = (3.95, 6.05).
1865        let cq = fixture();
1866        let g = dense_vals(&cq.curr_grad_barrier_obj_x());
1867        assert!((g[0] - 3.95).abs() < 1e-13);
1868        assert!((g[1] - 6.05).abs() < 1e-13);
1869    }
1870
1871    #[test]
1872    fn grad_lag_s_is_minus_y_d_minus_pl_v_l_plus_pu_v_u() {
1873        // tmp = P_U v_U = (zero-dim contrib) → 0
1874        // tmp -= P_L v_L → tmp = -[0.3]
1875        // tmp -= y_d = -[0.3] - [1.0] = [-1.3]
1876        let cq = fixture();
1877        assert!((dense_vals(&cq.curr_grad_lag_s())[0] + 1.3).abs() < 1e-15);
1878    }
1879
1880    fn zero_iv_like(iv: &IteratesVector) -> IteratesVector {
1881        // Materialize explicit zeros for every component so the
1882        // affine-step tests can compose direct-sum updates.
1883        IteratesVector::new(
1884            rcv(&vec![0.0; iv.x.dim() as usize]),
1885            rcv(&vec![0.0; iv.s.dim() as usize]),
1886            rcv(&vec![0.0; iv.y_c.dim() as usize]),
1887            rcv(&vec![0.0; iv.y_d.dim() as usize]),
1888            rcv(&vec![0.0; iv.z_l.dim() as usize]),
1889            rcv(&vec![0.0; iv.z_u.dim() as usize]),
1890            rcv(&vec![0.0; iv.v_l.dim() as usize]),
1891            rcv(&vec![0.0; iv.v_u.dim() as usize]),
1892        )
1893    }
1894
1895    #[test]
1896    fn aff_step_compl_avrg_with_zero_step_matches_curr_avrg_compl() {
1897        // Δ_aff = 0 ⇒ predicted compl ≡ current compl.
1898        // s_X_L · z_L = 2·0.5=1, s_X_U·z_U=2·0.7=1.4, s_S_L·v_L=3·0.3=0.9.
1899        // Total = 3.3; N = 3 (z_l + z_u + v_l, v_u empty); avrg = 1.1.
1900        let cq = fixture();
1901        let iv = cq.curr_iv();
1902        let zero = zero_iv_like(&iv);
1903        let m = cq.aff_step_compl_avrg(&zero, 1.0, 1.0);
1904        assert!((m - 1.1).abs() < 1e-13);
1905        assert!((cq.curr_avrg_compl() - 1.1).abs() < 1e-13);
1906    }
1907
1908    #[test]
1909    fn aff_step_compl_avrg_responds_to_primal_step() {
1910        // Δ_aff.x = (1, 0), α_pri = 1, others = 0.
1911        // s_X_L_aff = 2 + 1·1 = 3; s_X_U_aff = 2 (P_U^T·dx = 0); s_S_L_aff = 3.
1912        // (3·0.5 + 2·0.7 + 3·0.3) / 3 = (1.5 + 1.4 + 0.9) / 3 = 1.2667.
1913        let cq = fixture();
1914        let iv = cq.curr_iv();
1915        let mut z = zero_iv_like(&iv);
1916        z.x = rcv(&[1.0, 0.0]);
1917        let m = cq.aff_step_compl_avrg(&z, 1.0, 1.0);
1918        assert!((m - 1.2666666666666666).abs() < 1e-13);
1919    }
1920
1921    #[test]
1922    fn aff_step_alpha_primal_truncates_to_x_lower_bound() {
1923        // Δ_aff.x = (-3, 0); s_X_L = 2; tau = 1 ⇒ α_max = 2/3.
1924        let cq = fixture();
1925        let iv = cq.curr_iv();
1926        let mut z = zero_iv_like(&iv);
1927        z.x = rcv(&[-3.0, 0.0]);
1928        let a = cq.aff_step_alpha_primal_max(&z, 1.0);
1929        assert!((a - 2.0 / 3.0).abs() < 1e-13);
1930    }
1931
1932    #[test]
1933    fn aff_step_alpha_dual_truncates_to_z_lower_bound() {
1934        // Δ_aff.z_L = (-1); z_L = 0.5; tau = 1 ⇒ α_max = 0.5.
1935        let cq = fixture();
1936        let iv = cq.curr_iv();
1937        let mut z = zero_iv_like(&iv);
1938        z.z_l = rcv(&[-1.0]);
1939        let a = cq.aff_step_alpha_dual_max(&z, 1.0);
1940        assert!((a - 0.5).abs() < 1e-13);
1941    }
1942
1943    #[test]
1944    fn grad_barr_t_delta_dots_barrier_grads_with_step() {
1945        // ∇_x φ = (3.95, 6.05); ∇_s φ = (-mu/s_s_L) = -0.1/3 ≈ -0.03333…
1946        // δx = (1, 2); δs = (3): result = 3.95·1 + 6.05·2 + (-0.0333…)·3
1947        //                              = 3.95 + 12.10 − 0.1 = 15.95.
1948        let cq = fixture();
1949        let dx = dvec(&[1.0, 2.0]);
1950        let ds = dvec(&[3.0]);
1951        let r = cq.curr_grad_barr_t_delta(&dx, &ds);
1952        let expected = 3.95 + 12.10 - 0.1;
1953        assert!((r - expected).abs() < 1e-13, "r = {r}");
1954    }
1955
1956    #[test]
1957    fn dwd_with_no_w_collapses_to_sigma_quadratic() {
1958        // W is None in the fixture (no Hessian seeded), perts default to 0.
1959        // σ_x = (0.25, 0.35); σ_s = (0.1).
1960        // δx = (2, -1); δs = (3) ⇒ dWd = 0.25·4 + 0.35·1 + 0.1·9
1961        //                              = 1.00 + 0.35 + 0.90 = 2.25.
1962        let cq = fixture();
1963        let dx = dvec(&[2.0, -1.0]);
1964        let ds = dvec(&[3.0]);
1965        let r = cq.curr_dwd(&dx, &ds);
1966        assert!((r - 2.25).abs() < 1e-13, "r = {r}");
1967    }
1968
1969    #[test]
1970    fn dwd_includes_pd_perturbations() {
1971        // Without perts: dWd = 0.25·4 + 0.35·1 + 0.1·9 = 2.25.
1972        // δ_pert_x = 0.5, δ_pert_s = 0.25:
1973        //   add δ_pert_x · ‖δx‖² + δ_pert_s · ‖δs‖²
1974        //     = 0.5·(4+1) + 0.25·9 = 2.5 + 2.25 = 4.75.
1975        // Total = 7.00.
1976        let cq = fixture();
1977        {
1978            let mut d = cq.data.borrow_mut();
1979            d.perturbations.delta_x = 0.5;
1980            d.perturbations.delta_s = 0.25;
1981        }
1982        let dx = dvec(&[2.0, -1.0]);
1983        let ds = dvec(&[3.0]);
1984        let r = cq.curr_dwd(&dx, &ds);
1985        assert!((r - 7.00).abs() < 1e-13, "r = {r}");
1986    }
1987}