Skip to main content

pounce_algorithm/init/
default.rs

1//! Default iterate initializer — port of
2//! `Algorithm/IpDefaultIterateInitializer.{hpp,cpp}`.
3//!
4//! Bound push, slack init, multiplier init (constant / mu-based /
5//! least-square via the `EqMultCalculator`). Constants below match
6//! upstream's defaults from `RegisterOptions`.
7//!
8//! `set_initial_iterates` ports the upstream sequence:
9//!
10//! 1. Pull `x` from `nlp.get_starting_x` and push each component
11//!    into the interior of `[x_l, x_u]` per
12//!    [`DefaultIterateInitializer::push_to_interior`].
13//! 2. Set `s = d(x)` (evaluated through CQ on a transient iterate)
14//!    and push it into the interior of `[d_l, d_u]`.
15//! 3. Initialize `y_c`, `y_d`:
16//!    * `bound_mult_init_method == "constant"` — leave at zero (the
17//!      default y-targets that the linear-solver sweep will refine).
18//!    * `bound_mult_init_method == "least-square"` — call
19//!      [`crate::eq_mult::least_square::LeastSquareMults`] via the
20//!      provided `aug_solver`. **Phase 7 default is "constant"** to
21//!      avoid pulling the aug-system through this path on bring-up.
22//! 4. Initialize `z_l`, `z_u`, `v_l`, `v_u` to `bound_mult_init_val`
23//!    (component-wise).
24//!
25//! The fully-loaded mu-based / least-square multiplier paths land
26//! once `LeastSquareMults` and the iterate-trace gold tests are
27//! online.
28
29use crate::eq_mult::r#trait::EqMultCalculator;
30use crate::init::r#trait::IterateInitializer;
31use crate::ipopt_cq::IpoptCqHandle;
32use crate::ipopt_data::IpoptDataHandle;
33use crate::ipopt_nlp::IpoptNlp;
34use crate::iterates_vector::IteratesVector;
35use crate::kkt::aug_system_solver::AugSystemSolver;
36use pounce_common::types::Number;
37use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
38use pounce_linalg::Vector;
39use std::cell::RefCell;
40use std::rc::Rc;
41
42pub struct DefaultIterateInitializer {
43    pub bound_push: Number,
44    pub bound_frac: Number,
45    pub slack_bound_push: Number,
46    pub slack_bound_frac: Number,
47    pub constr_mult_init_max: Number,
48    pub bound_mult_init_val: Number,
49    /// "constant" / "mu-based" / "least-square".
50    pub bound_mult_init_method: String,
51    /// Equality-multiplier calculator used by the
52    /// `least_square_mults` step at the end of `set_initial_iterates`,
53    /// matching upstream `IpDefaultIterateInitializer.cpp:334-341`. If
54    /// `None`, the LS step is skipped (y_c, y_d remain at zero).
55    pub eq_mult_calculator: Option<Box<dyn EqMultCalculator>>,
56}
57
58impl Default for DefaultIterateInitializer {
59    fn default() -> Self {
60        Self {
61            bound_push: 1e-2,
62            bound_frac: 1e-2,
63            slack_bound_push: 1e-2,
64            slack_bound_frac: 1e-2,
65            constr_mult_init_max: 1e3,
66            bound_mult_init_val: 1.0,
67            bound_mult_init_method: "constant".into(),
68            eq_mult_calculator: None,
69        }
70    }
71}
72
73impl DefaultIterateInitializer {
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    pub fn with_eq_mult_calculator(eq_mult: Box<dyn EqMultCalculator>) -> Self {
79        Self {
80            eq_mult_calculator: Some(eq_mult),
81            ..Self::default()
82        }
83    }
84
85    /// Per-element bound-push formula from upstream
86    /// `IpDefaultIterateInitializer.cpp:473-666`. Given a primal value
87    /// `x` and optional bounds `(lower, upper)`, return a value
88    /// shifted to the interior:
89    ///
90    /// * Two-sided bounds: clamp into `[lo + p_l, hi - p_u]` where
91    ///   `p_l = min(bound_push * max(|lo|, 1), bound_frac * (hi-lo))`,
92    ///   `p_u = min(bound_push * max(|hi|, 1), bound_frac * (hi-lo))`.
93    /// * Lower-only: return `max(x, lo + bound_push * max(|lo|, 1))`.
94    /// * Upper-only: return `min(x, hi - bound_push * max(|hi|, 1))`.
95    /// * Free: return `x`.
96    ///
97    /// The `Px_L`/`Px_U` selection-matrix dance in upstream collapses
98    /// to exactly this per-coordinate formula once the bounds are
99    /// expanded to the full primal space.
100    pub fn push_to_interior(
101        bound_push: Number,
102        bound_frac: Number,
103        x: Number,
104        lower: Option<Number>,
105        upper: Option<Number>,
106    ) -> Number {
107        match (lower, upper) {
108            (Some(lo), Some(hi)) => {
109                let span = hi - lo;
110                let p_l = (bound_push * lo.abs().max(1.0)).min(bound_frac * span);
111                let p_u = (bound_push * hi.abs().max(1.0)).min(bound_frac * span);
112                x.max(lo + p_l).min(hi - p_u)
113            }
114            (Some(lo), None) => {
115                let p_l = bound_push * lo.abs().max(1.0);
116                x.max(lo + p_l)
117            }
118            (None, Some(hi)) => {
119                let p_u = bound_push * hi.abs().max(1.0);
120                x.min(hi - p_u)
121            }
122            (None, None) => x,
123        }
124    }
125}
126
127impl IterateInitializer for DefaultIterateInitializer {
128    fn set_initial_iterates(
129        &mut self,
130        data: &IpoptDataHandle,
131        cq: &IpoptCqHandle,
132        nlp: &Rc<RefCell<dyn IpoptNlp>>,
133        aug_solver: &mut dyn AugSystemSolver,
134    ) -> bool {
135        let curr_template = match data.borrow().curr.clone() {
136            Some(c) => c,
137            None => return false,
138        };
139
140        let n_x = curr_template.x.dim();
141        let n_s = curr_template.s.dim();
142        let n_yc = curr_template.y_c.dim();
143        let n_yd = curr_template.y_d.dim();
144        let n_zl = curr_template.z_l.dim();
145        let n_zu = curr_template.z_u.dim();
146        let n_vl = curr_template.v_l.dim();
147        let n_vu = curr_template.v_u.dim();
148
149        // Step 1: pull x from NLP and push each finite-bounded
150        // component into the interior. Bound vectors `x_l`, `x_u` are
151        // packed (only finite entries); we expand via `Px_L^T` masks
152        // by walking the dense slot.
153        let mut x = DenseVectorSpace::new(n_x).make_new_dense();
154        nlp.borrow_mut().get_starting_x(&mut x);
155        {
156            let nlp_ref = nlp.borrow();
157            push_x_into_interior(
158                &mut x,
159                &*nlp_ref.px_l(),
160                nlp_ref.x_l(),
161                &*nlp_ref.px_u(),
162                nlp_ref.x_u(),
163                self.bound_push,
164                self.bound_frac,
165            );
166        }
167
168        // Step 2: s = d(x), then push into [d_l, d_u].
169        let mut s = DenseVectorSpace::new(n_s).make_new_dense();
170        nlp.borrow_mut().eval_d(&x, &mut s);
171        {
172            let nlp_ref = nlp.borrow();
173            push_x_into_interior(
174                &mut s,
175                &*nlp_ref.pd_l(),
176                nlp_ref.d_l(),
177                &*nlp_ref.pd_u(),
178                nlp_ref.d_u(),
179                self.slack_bound_push,
180                self.slack_bound_frac,
181            );
182        }
183
184        // Step 3: y_c, y_d initial guesses. `constant` mode leaves
185        // them at zero (the algorithm refines on the first KKT solve).
186        let mut y_c = DenseVectorSpace::new(n_yc).make_new_dense();
187        let mut y_d = DenseVectorSpace::new(n_yd).make_new_dense();
188        if self.bound_mult_init_method == "constant" {
189            // Materialize as homogeneous-zero so callers' asum / values
190            // probes don't trip the `initialized` debug-assert.
191            y_c.set(0.0);
192            y_d.set(0.0);
193        } else {
194            // Other modes (mu-based, least-square) require the
195            // aug-system path; fall back to NLP's own y-init for now.
196            nlp.borrow_mut().get_starting_y(&mut y_c, &mut y_d);
197            cap_constraint_multipliers(&mut y_c, self.constr_mult_init_max);
198            cap_constraint_multipliers(&mut y_d, self.constr_mult_init_max);
199        }
200
201        // Step 4: bound multipliers — constant init.
202        let mut z_l = DenseVectorSpace::new(n_zl).make_new_dense();
203        let mut z_u = DenseVectorSpace::new(n_zu).make_new_dense();
204        let mut v_l = DenseVectorSpace::new(n_vl).make_new_dense();
205        let mut v_u = DenseVectorSpace::new(n_vu).make_new_dense();
206        z_l.set(self.bound_mult_init_val);
207        z_u.set(self.bound_mult_init_val);
208        v_l.set(self.bound_mult_init_val);
209        v_u.set(self.bound_mult_init_val);
210
211        let iv = IteratesVector::new(
212            Rc::new(x),
213            Rc::new(s),
214            Rc::new(y_c),
215            Rc::new(y_d),
216            Rc::new(z_l),
217            Rc::new(z_u),
218            Rc::new(v_l),
219            Rc::new(v_u),
220        );
221        let n_x_dim = iv.x.dim();
222        data.borrow_mut().set_curr(iv);
223
224        // Step 5: least-square equality multipliers — port of
225        // `IpDefaultIterateInitializer.cpp:285-341` /
226        // `least_square_mults` (lines 669-743). Upstream always runs
227        // this after the constant-init y_c/y_d=0, unless the full
228        // `least_square_init_duals` path succeeded. Without it the
229        // initial gradient-of-Lagrangian residual is computed against
230        // y_c=y_d=0, blowing up `inf_du` on iter 0.
231        if n_yc != n_x_dim
232            && self.constr_mult_init_max > 0.0
233            && (n_yc + n_yd) > 0
234            && self.eq_mult_calculator.is_some()
235        {
236            let mut new_y_c = DenseVectorSpace::new(n_yc).make_new_dense();
237            let mut new_y_d = DenseVectorSpace::new(n_yd).make_new_dense();
238            let calc = self.eq_mult_calculator.as_mut().unwrap();
239            let ok = calc.calculate_y_eq(data, cq, nlp, aug_solver, &mut new_y_c, &mut new_y_d);
240            if !ok {
241                // Solver failed → leave at zero (already the case).
242                data.borrow_mut().append_info_string("y0");
243            } else {
244                let yinitnrm = new_y_c.amax().max(new_y_d.amax());
245                if yinitnrm > self.constr_mult_init_max {
246                    // Cap exceeded → upstream zeros them out
247                    // (`IpDefaultIterateInitializer.cpp:723-727`).
248                    data.borrow_mut().append_info_string("yc");
249                } else {
250                    // Accept LS estimates. Build a fresh iterate
251                    // sharing the existing x/s/z/v Rcs and replacing
252                    // y_c, y_d with the LS values.
253                    let curr = data.borrow().curr.clone();
254                    if let Some(c) = curr {
255                        let new_iv = IteratesVector::new(
256                            c.x.clone(),
257                            c.s.clone(),
258                            Rc::new(new_y_c),
259                            Rc::new(new_y_d),
260                            c.z_l.clone(),
261                            c.z_u.clone(),
262                            c.v_l.clone(),
263                            c.v_u.clone(),
264                        );
265                        let mut d = data.borrow_mut();
266                        d.set_curr(new_iv);
267                        d.append_info_string("y");
268                    }
269                }
270            }
271        }
272
273        true
274    }
275}
276
277/// Apply [`DefaultIterateInitializer::push_to_interior`] to every
278/// component of `x` using the lower/upper bound vectors expanded
279/// through the `P_L`/`P_U` selection matrices. Bounds are packed
280/// (lower-bound vector `x_l` has dim equal to the number of
281/// lower-bounded components; `Px_L: n × n_lo` selects them).
282pub(crate) fn push_x_into_interior(
283    x: &mut DenseVector,
284    px_l: &dyn pounce_linalg::Matrix,
285    x_l: &dyn Vector,
286    px_u: &dyn pounce_linalg::Matrix,
287    x_u: &dyn Vector,
288    bound_push: Number,
289    bound_frac: Number,
290) {
291    // Use `dim()` (not `values().len()`): the iterate initializer is
292    // called before any user `x0` has been written, so `x` is still in
293    // its default homogeneous-zero state. `values()` carries a
294    // `debug_assert!(!self.homogeneous)` and trips in debug builds on
295    // clnlbeam.nl-class problems (n=59999, x_L/x_U packed). `values_mut()`
296    // below materializes the dense buffer before the per-element write.
297    let n = x.dim() as usize;
298    // Expand x_l and x_u into full-length sentinel vectors:
299    //   lower[i] = Some(x_l_packed[k]) if i is the k-th lower-bounded slot
300    //   upper[i] = Some(x_u_packed[k]) similarly.
301    let mut lower = vec![None; n];
302    let mut upper = vec![None; n];
303    expand_packed_into_dense(px_l, x_l, &mut lower);
304    expand_packed_into_dense(px_u, x_u, &mut upper);
305
306    let xs = x.values_mut();
307    for (i, xi) in xs.iter_mut().enumerate() {
308        *xi = DefaultIterateInitializer::push_to_interior(
309            bound_push, bound_frac, *xi, lower[i], upper[i],
310        );
311    }
312}
313
314/// Apply `P` to a packed bound vector `b_packed` (dim `n_pack`) to
315/// produce a sparse marking of `out` (dim `P.n_rows`). For each
316/// `k = 0..n_pack`, `out[P_rows[k]] = Some(b_packed[k])`. Falls back
317/// to a column-by-column probe via `mult_vector` if downcast to
318/// `ExpansionMatrix` is unavailable.
319fn expand_packed_into_dense(
320    p: &dyn pounce_linalg::Matrix,
321    b_packed: &dyn Vector,
322    out: &mut [Option<Number>],
323) {
324    use pounce_linalg::expansion_matrix::ExpansionMatrix;
325    let dim_packed = b_packed.dim() as usize;
326    if dim_packed == 0 {
327        return;
328    }
329
330    if let Some(em) = p.as_any().downcast_ref::<ExpansionMatrix>() {
331        let rows = em.expanded_pos_indices();
332        let Some(packed) = b_packed.as_any().downcast_ref::<DenseVector>() else {
333            unreachable!("expansion-matrix bound vec must be DenseVector")
334        };
335        let vals = packed.values();
336        for k in 0..dim_packed {
337            let row = rows[k] as usize;
338            out[row] = Some(vals[k]);
339        }
340    } else {
341        // Generic fallback: probe via mult_vector with unit input
342        // vectors. Quadratic; fine for tiny problems and tests.
343        let n_full = out.len() as i32;
344        let mut tmp = DenseVectorSpace::new(n_full).make_new_dense();
345        for k in 0..dim_packed {
346            let mut e_k = DenseVectorSpace::new(b_packed.dim()).make_new_dense();
347            e_k.values_mut()[k] = 1.0;
348            tmp.set(0.0);
349            p.mult_vector(1.0, &e_k, 0.0, &mut tmp);
350            // tmp is the k-th expansion column: a single 1.0 at the
351            // expanded position. Read the value we want into the
352            // matching slot.
353            let Some(packed) = b_packed.as_any().downcast_ref::<DenseVector>() else {
354                unreachable!("packed bound vec must be DenseVector")
355            };
356            for (i, &t) in tmp.values().iter().enumerate() {
357                if t == 1.0 {
358                    out[i] = Some(packed.values()[k]);
359                }
360            }
361        }
362    }
363}
364
365/// Clamp every component of `y` to `[-max, max]`. Mirrors the
366/// upstream `constr_mult_init_max` cap.
367fn cap_constraint_multipliers(y: &mut DenseVector, max: Number) {
368    for v in y.values_mut() {
369        if *v > max {
370            *v = max;
371        } else if *v < -max {
372            *v = -max;
373        }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn interior_point_left_alone() {
383        // x=5 strictly inside [0, 10] with bound_push=1e-2 →
384        // p_l = min(1e-2 * max(0,1), 1e-2 * 10) = 1e-2; same for p_u.
385        // 5 is well inside [0.01, 9.9].
386        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 5.0, Some(0.0), Some(10.0));
387        assert!((v - 5.0).abs() < 1e-15);
388    }
389
390    #[test]
391    fn point_at_lower_bound_pushed_in() {
392        // x=0 at the lower bound. Should become lo + p_l = 0.01.
393        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 0.0, Some(0.0), Some(10.0));
394        assert!((v - 0.01).abs() < 1e-15);
395    }
396
397    #[test]
398    fn point_at_upper_bound_pushed_in() {
399        // x=10 at the upper bound. Should become hi - p_u = 9.9.
400        let v =
401            DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 10.0, Some(0.0), Some(10.0));
402        assert!((v - 9.9).abs() < 1e-15);
403    }
404
405    #[test]
406    fn point_below_lower_bound_clamped() {
407        // x=-5 → lo + p_l = 0.01.
408        let v =
409            DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, -5.0, Some(0.0), Some(10.0));
410        assert!((v - 0.01).abs() < 1e-15);
411    }
412
413    #[test]
414    fn lower_only_pushed_by_max_abs() {
415        // Lower-only with lo=-100: p_l = bound_push * max(|-100|, 1) = 1e-2 * 100 = 1.
416        // x=-100 → -100 + 1 = -99.
417        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, -100.0, Some(-100.0), None);
418        assert!((v - -99.0).abs() < 1e-13);
419    }
420
421    #[test]
422    fn upper_only_pushed_by_max_abs() {
423        // Upper-only with hi=50, x=50 → 50 - 1e-2 * 50 = 49.5.
424        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 50.0, None, Some(50.0));
425        assert!((v - 49.5).abs() < 1e-13);
426    }
427
428    #[test]
429    fn free_variable_unchanged() {
430        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 42.0, None, None);
431        assert_eq!(v, 42.0);
432    }
433
434    #[test]
435    fn narrow_interval_uses_bound_frac_branch() {
436        // Tiny span [0, 1e-4]: p_l = min(1e-2 * 1, 1e-2 * 1e-4) = 1e-6.
437        // x=0 → 0 + 1e-6 = 1e-6.
438        let v = DefaultIterateInitializer::push_to_interior(1e-2, 1e-2, 0.0, Some(0.0), Some(1e-4));
439        assert!((v - 1e-6).abs() < 1e-18);
440    }
441}