Skip to main content

pounce_algorithm/kkt/
slack_scaling.rs

1//! Slack-based symmetric scaling — port of
2//! `Algorithm/LinearSolvers/IpSlackBasedTSymScalingMethod.{hpp,cpp}`.
3//!
4//! Used by the inexact algorithm (and any caller setting
5//! `linear_system_scaling=slack-based`). Operates on the augmented KKT
6//! system whose row block layout is
7//!
8//! ```text
9//!   [ x  (nx) | s  (ns) | y_c (nc) | y_d (nd) ]
10//! ```
11//!
12//! Per-row factors:
13//!
14//! * x-rows, y_c-rows, y_d-rows → `1.0`
15//! * s-rows → `min(1.0, P_d_L · slack_s_L + P_d_U · slack_s_U)` per
16//!   row, i.e. damp the s-row by the active slack value when small.
17//!
18//! Lives in `pounce-algorithm` rather than `pounce-linsol` because the
19//! method needs access to `IpoptCalculatedQuantities` (current slacks)
20//! and the `IpoptNlp` bound-expansion matrices, which would otherwise
21//! create a circular crate dependency.
22
23use crate::ipopt_cq::IpoptCqHandle;
24use pounce_common::types::{Index, Number};
25use pounce_linalg::dense_vector::DenseVector;
26use pounce_linsol::scaling::TSymScalingMethod;
27
28pub struct SlackBasedTSymScalingMethod {
29    cq: IpoptCqHandle,
30    /// Mirrors the upstream hard-coded `slack_scale_max = 1.0`. Kept as
31    /// a field so the scalar core stays inspectable in tests.
32    pub slack_scale_max: Number,
33}
34
35impl SlackBasedTSymScalingMethod {
36    pub fn new(cq: IpoptCqHandle) -> Self {
37        Self {
38            cq,
39            slack_scale_max: 1.0,
40        }
41    }
42}
43
44impl TSymScalingMethod for SlackBasedTSymScalingMethod {
45    fn compute_sym_t_scaling_factors(
46        &mut self,
47        n: Index,
48        _nnz: Index,
49        _airn: &[Index],
50        _ajcn: &[Index],
51        _a: &[Number],
52        scaling_factors: &mut [Number],
53    ) -> bool {
54        let cq_ref = self.cq.borrow();
55        let (s_template, nx, ns, nc, nd) = {
56            let data = cq_ref.data().borrow();
57            let curr = data
58                .curr
59                .as_ref()
60                .unwrap_or_else(|| panic!("SlackBasedTSymScalingMethod: curr iterate missing"));
61            (
62                curr.s.clone(),
63                curr.x.dim(),
64                curr.s.dim(),
65                curr.y_c.dim(),
66                curr.y_d.dim(),
67            )
68        };
69        debug_assert_eq!(n, nx + ns + nc + nd);
70        debug_assert_eq!(scaling_factors.len(), n as usize);
71
72        // x-rows: identity.
73        for s in scaling_factors[..nx as usize].iter_mut() {
74            *s = 1.0;
75        }
76
77        // s-rows: tmp = Pd_L * slack_s_L + Pd_U * slack_s_U; then
78        // tmp = min(tmp, slack_scale_max) elementwise.
79        let curr_slack_s_l = cq_ref.curr_slack_s_l();
80        let curr_slack_s_u = cq_ref.curr_slack_s_u();
81        let nlp = cq_ref.nlp().borrow();
82        let pd_l = nlp.pd_l();
83        let pd_u = nlp.pd_u();
84
85        let mut tmp = s_template.make_new();
86        pd_l.mult_vector(1.0, &*curr_slack_s_l, 0.0, &mut *tmp);
87        pd_u.mult_vector(1.0, &*curr_slack_s_u, 1.0, &mut *tmp);
88
89        let mut bound = tmp.make_new();
90        bound.set(self.slack_scale_max);
91        tmp.element_wise_min(&*bound);
92
93        let dense = tmp
94            .as_any()
95            .downcast_ref::<DenseVector>()
96            .unwrap_or_else(|| {
97                panic!("SlackBasedTSymScalingMethod: slack vector is not a DenseVector")
98            });
99        let vals = dense.expanded_values();
100        scaling_factors[nx as usize..(nx + ns) as usize].copy_from_slice(&vals);
101
102        // y_c / y_d rows: identity.
103        for s in scaling_factors[(nx + ns) as usize..].iter_mut() {
104            *s = 1.0;
105        }
106
107        true
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::ipopt_cq::IpoptCalculatedQuantities;
115    use crate::ipopt_data::IpoptData;
116    use crate::ipopt_nlp::{IpoptNlp, Nlp};
117    use crate::iterates_vector::IteratesVector;
118    use pounce_common::types::{Index, Number};
119    use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
120    use pounce_linalg::special_matrix::IdentityMatrix;
121    use pounce_linalg::{Matrix, SymMatrix, Vector};
122    use std::cell::RefCell;
123    use std::rc::Rc;
124
125    fn dvec(values: &[Number]) -> DenseVector {
126        let space = DenseVectorSpace::new(values.len() as Index);
127        let mut v = space.make_new_dense();
128        if !values.is_empty() {
129            v.values_mut().copy_from_slice(values);
130        }
131        v
132    }
133
134    fn rcv(values: &[Number]) -> Rc<dyn Vector> {
135        Rc::new(dvec(values))
136    }
137
138    /// Minimal IpoptNlp stub. All slacks are lower- and upper-bounded by
139    /// identity expansion matrices (so `Pd_L = Pd_U = I` of dim `ns`).
140    #[derive(Debug)]
141    struct StubNlp {
142        n_x: Index,
143        m_eq: Index,
144        m_ineq: Index,
145        x_l: DenseVector,
146        x_u: DenseVector,
147        d_l: DenseVector,
148        d_u: DenseVector,
149        px_l: Rc<dyn Matrix>,
150        px_u: Rc<dyn Matrix>,
151        pd_l: Rc<dyn Matrix>,
152        pd_u: Rc<dyn Matrix>,
153    }
154
155    impl Nlp for StubNlp {
156        fn n(&self) -> Index {
157            self.n_x
158        }
159        fn m_eq(&self) -> Index {
160            self.m_eq
161        }
162        fn m_ineq(&self) -> Index {
163            self.m_ineq
164        }
165        fn eval_f(&mut self, _x: &dyn Vector) -> Number {
166            0.0
167        }
168        fn eval_grad_f(&mut self, _x: &dyn Vector, _g: &mut dyn Vector) {}
169        fn eval_c(&mut self, _x: &dyn Vector, _c: &mut dyn Vector) {}
170        fn eval_d(&mut self, _x: &dyn Vector, _d: &mut dyn Vector) {}
171        fn eval_jac_c(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
172            unimplemented!()
173        }
174        fn eval_jac_d(&mut self, _x: &dyn Vector) -> Rc<dyn Matrix> {
175            unimplemented!()
176        }
177        fn eval_h(
178            &mut self,
179            _x: &dyn Vector,
180            _obj_factor: Number,
181            _y_c: &dyn Vector,
182            _y_d: &dyn Vector,
183        ) -> Rc<dyn SymMatrix> {
184            unimplemented!()
185        }
186    }
187
188    impl IpoptNlp for StubNlp {
189        fn x_l(&self) -> &dyn Vector {
190            &self.x_l
191        }
192        fn x_u(&self) -> &dyn Vector {
193            &self.x_u
194        }
195        fn d_l(&self) -> &dyn Vector {
196            &self.d_l
197        }
198        fn d_u(&self) -> &dyn Vector {
199            &self.d_u
200        }
201        fn px_l(&self) -> Rc<dyn Matrix> {
202            self.px_l.clone()
203        }
204        fn px_u(&self) -> Rc<dyn Matrix> {
205            self.px_u.clone()
206        }
207        fn pd_l(&self) -> Rc<dyn Matrix> {
208            self.pd_l.clone()
209        }
210        fn pd_u(&self) -> Rc<dyn Matrix> {
211            self.pd_u.clone()
212        }
213    }
214
215    fn build_cq(
216        x: Rc<dyn Vector>,
217        s: Rc<dyn Vector>,
218        y_c: Rc<dyn Vector>,
219        y_d: Rc<dyn Vector>,
220        nlp: StubNlp,
221    ) -> IpoptCqHandle {
222        // The unused multiplier blocks (z_l, z_u, v_l, v_u) are sized
223        // arbitrarily; the slack-scaling code never reads them.
224        let nx = x.dim();
225        let ns = s.dim();
226        let z = rcv(&vec![0.0; nx as usize]);
227        let v = rcv(&vec![0.0; ns as usize]);
228        let iv = IteratesVector::new(x, s, y_c, y_d, z.clone(), z, v.clone(), v);
229        let mut data = IpoptData::new();
230        data.set_curr(iv);
231        let data_handle = Rc::new(RefCell::new(data));
232        let nlp_handle: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(nlp));
233        let cq = IpoptCalculatedQuantities::new(data_handle, nlp_handle);
234        Rc::new(RefCell::new(cq))
235    }
236
237    #[test]
238    fn slack_scaling_block_layout_with_loose_bounds_is_all_ones() {
239        // nx=2, ns=2, nc=0, nd=2.
240        // s = [0.5, 1.5], d_l = [0, 0], d_u = [10, 10].
241        // slack_s_L = s - d_l = [0.5, 1.5];  slack_s_U = d_u - s = [9.5, 8.5]
242        // Pd_L = Pd_U = I  ⇒  tmp = slack_s_L + slack_s_U = [10, 10].
243        // Capped at slack_scale_max=1 ⇒ s-rows = [1.0, 1.0].
244        let nlp = StubNlp {
245            n_x: 2,
246            m_eq: 0,
247            m_ineq: 2,
248            x_l: dvec(&[]),
249            x_u: dvec(&[]),
250            d_l: dvec(&[0.0, 0.0]),
251            d_u: dvec(&[10.0, 10.0]),
252            px_l: Rc::new(IdentityMatrix::new(0)),
253            px_u: Rc::new(IdentityMatrix::new(0)),
254            pd_l: Rc::new(IdentityMatrix::new(2)),
255            pd_u: Rc::new(IdentityMatrix::new(2)),
256        };
257        // Note: px_l/px_u use 0×0 identity since x has no bounds in
258        // this fixture; the slack-scaling path never invokes them.
259        let cq = build_cq(
260            rcv(&[0.0, 0.0]),
261            rcv(&[0.5, 1.5]),
262            rcv(&[]),
263            rcv(&[0.0, 0.0]),
264            nlp,
265        );
266
267        let mut method = SlackBasedTSymScalingMethod::new(cq);
268        let mut sf = vec![0.0; 6];
269        assert!(method.compute_sym_t_scaling_factors(6, 0, &[], &[], &[], &mut sf));
270        assert_eq!(&sf[0..2], &[1.0, 1.0]);
271        assert_eq!(&sf[2..4], &[1.0, 1.0]);
272        assert_eq!(&sf[4..6], &[1.0, 1.0]);
273    }
274
275    #[test]
276    fn slack_scaling_caps_small_slack() {
277        // nx=1, ns=2, nc=0, nd=2.
278        // d_l = [0, 0], d_u = [0.6, 0.6], s = [0.5, 0.5].
279        // slack_s_L = [0.5, 0.5];  slack_s_U = [0.1, 0.1]
280        // tmp = [0.6, 0.6]; min(1.0) = [0.6, 0.6].
281        let nlp = StubNlp {
282            n_x: 1,
283            m_eq: 0,
284            m_ineq: 2,
285            x_l: dvec(&[]),
286            x_u: dvec(&[]),
287            d_l: dvec(&[0.0, 0.0]),
288            d_u: dvec(&[0.6, 0.6]),
289            px_l: Rc::new(IdentityMatrix::new(0)),
290            px_u: Rc::new(IdentityMatrix::new(0)),
291            pd_l: Rc::new(IdentityMatrix::new(2)),
292            pd_u: Rc::new(IdentityMatrix::new(2)),
293        };
294        let cq = build_cq(
295            rcv(&[0.0]),
296            rcv(&[0.5, 0.5]),
297            rcv(&[]),
298            rcv(&[0.0, 0.0]),
299            nlp,
300        );
301
302        let mut method = SlackBasedTSymScalingMethod::new(cq);
303        let mut sf = vec![0.0; 5];
304        assert!(method.compute_sym_t_scaling_factors(5, 0, &[], &[], &[], &mut sf));
305        assert_eq!(sf[0], 1.0);
306        assert!((sf[1] - 0.6).abs() < 1e-15);
307        assert!((sf[2] - 0.6).abs() < 1e-15);
308        assert_eq!(&sf[3..5], &[1.0, 1.0]);
309    }
310
311    #[test]
312    fn slack_scaling_with_only_lower_bounds_uses_pd_l_only() {
313        // nx=1, ns=2, nc=1, nd=2.
314        // Only lower bounds present (Pd_U has 0 columns).
315        // d_l = [0.1, 0.1], s = [0.4, 0.7].
316        // slack_s_L = [0.3, 0.6]; tmp = [0.3, 0.6]; min(1.0) = [0.3, 0.6].
317        let nlp = StubNlp {
318            n_x: 1,
319            m_eq: 1,
320            m_ineq: 2,
321            x_l: dvec(&[]),
322            x_u: dvec(&[]),
323            d_l: dvec(&[0.1, 0.1]),
324            d_u: dvec(&[]),
325            px_l: Rc::new(IdentityMatrix::new(0)),
326            px_u: Rc::new(IdentityMatrix::new(0)),
327            pd_l: Rc::new(IdentityMatrix::new(2)),
328            // Pd_U is 2 rows × 0 cols — slack_s_U has dim 0, so the
329            // upper-bound contribution is empty.
330            pd_u: Rc::new(zero_matrix(2, 0)),
331        };
332        let cq = build_cq(
333            rcv(&[0.0]),
334            rcv(&[0.4, 0.7]),
335            rcv(&[0.0]),
336            rcv(&[0.0, 0.0]),
337            nlp,
338        );
339
340        let mut method = SlackBasedTSymScalingMethod::new(cq);
341        // Layout: 1 + 2 + 1 + 2 = 6.
342        let mut sf = vec![0.0; 6];
343        assert!(method.compute_sym_t_scaling_factors(6, 0, &[], &[], &[], &mut sf));
344        assert_eq!(sf[0], 1.0);
345        assert!((sf[1] - 0.3).abs() < 1e-15);
346        assert!((sf[2] - 0.6).abs() < 1e-15);
347        assert_eq!(&sf[3..6], &[1.0, 1.0, 1.0]);
348    }
349
350    fn zero_matrix(rows: Index, cols: Index) -> pounce_linalg::special_matrix::ZeroMatrix {
351        pounce_linalg::special_matrix::ZeroMatrix::new(rows, cols)
352    }
353}