Skip to main content

pounce_algorithm/init/
warm_start.rs

1//! Warm-start iterate initializer — port of
2//! `IpWarmStartIterateInitializer.{hpp,cpp}`. Used when a previous
3//! solve has left a trial point that should be reused.
4//!
5//! There are two callers we serve:
6//!
7//! * **`IpoptApplication::ReOptimizeTNLP`** (the upstream re-solve
8//!   path) populates `data.curr` with the previous solve's iterate.
9//!   We clamp multipliers and optionally override `mu`.
10//! * **First solves from `OptimizeTNLP`** that opt into
11//!   `warm_start_init_point=yes` to forward user-supplied
12//!   primal/dual seeds via `TNLP::get_starting_point`. Here
13//!   `data.curr` carries only dim metadata (uninitialized vectors);
14//!   we pull seeds from the NLP, push primals/slacks into the bound
15//!   interior with warm-start `bound_push`/`bound_frac`, and then
16//!   apply the same multiplier clamps.
17//!
18//! Wired options today: `bound_push`, `bound_frac`,
19//! `slack_bound_push`, `slack_bound_frac`, `mult_init_max`,
20//! `target_mu`. The remaining knobs (`mult_bound_push`,
21//! `entire_iterate`, `same_structure`) are stored but not yet
22//! consumed.
23
24use crate::alg_builder::WarmStartOptions;
25use crate::init::default::push_x_into_interior;
26use crate::init::r#trait::IterateInitializer;
27use crate::ipopt_cq::IpoptCqHandle;
28use crate::ipopt_data::IpoptDataHandle;
29use crate::ipopt_nlp::IpoptNlp;
30use crate::iterates_vector::IteratesVector;
31use crate::kkt::aug_system_solver::AugSystemSolver;
32use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
33use pounce_linalg::Vector;
34use std::cell::RefCell;
35use std::rc::Rc;
36
37pub struct WarmStartIterateInitializer {
38    opts: WarmStartOptions,
39}
40
41impl WarmStartIterateInitializer {
42    pub fn new() -> Self {
43        Self {
44            opts: WarmStartOptions::default(),
45        }
46    }
47
48    pub fn with_options(opts: WarmStartOptions) -> Self {
49        Self { opts }
50    }
51}
52
53impl Default for WarmStartIterateInitializer {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl IterateInitializer for WarmStartIterateInitializer {
60    fn set_initial_iterates(
61        &mut self,
62        data: &IpoptDataHandle,
63        _cq: &IpoptCqHandle,
64        nlp: &Rc<RefCell<dyn IpoptNlp>>,
65        _aug_solver: &mut dyn AugSystemSolver,
66    ) -> bool {
67        // Two entry points share this initializer: the re-optimize path
68        // (curr.x carries values from the prior solve) and the first
69        // OptimizeTNLP call that opted into warm_start_init_point=yes
70        // (curr.x is the application's placeholder seed — allocated but
71        // never written). Detect the latter and rebuild `curr` from the
72        // NLP's get_starting_x/y/z hooks before clamping.
73        let needs_seed_from_nlp = {
74            let borrow = data.borrow();
75            match borrow.curr.as_ref() {
76                None => return false,
77                Some(c) => !is_initialized(&c.x),
78            }
79        };
80
81        if needs_seed_from_nlp {
82            seed_from_nlp(data, nlp, &self.opts);
83        }
84
85        if self.opts.mult_init_max > 0.0 {
86            // Rebuild `curr` with clamped multipliers. Components are
87            // shared via `Rc` with previous solves, so we make fresh
88            // copies before mutating to avoid clobbering downstream
89            // borrowers.
90            let mut borrow = data.borrow_mut();
91            let curr = borrow.curr.as_ref().unwrap();
92            let cap = self.opts.mult_init_max;
93            let new_curr = IteratesVector::new(
94                Rc::clone(&curr.x),
95                Rc::clone(&curr.s),
96                clone_clamped(&curr.y_c, -cap, cap),
97                clone_clamped(&curr.y_d, -cap, cap),
98                clone_clamped(&curr.z_l, 0.0, cap),
99                clone_clamped(&curr.z_u, 0.0, cap),
100                clone_clamped(&curr.v_l, 0.0, cap),
101                clone_clamped(&curr.v_u, 0.0, cap),
102            );
103            borrow.set_curr(new_curr);
104        }
105
106        if self.opts.target_mu > 0.0 {
107            data.borrow_mut().curr_mu = self.opts.target_mu;
108        }
109
110        true
111    }
112}
113
114/// Pull a fresh starting iterate from the NLP (which routes to
115/// `TNLP::get_starting_point` with `init_x` / `init_lambda` /
116/// `init_z` all true), push the primals and slacks into the bound
117/// interior using warm-start-specific `bound_push`/`bound_frac`, and
118/// install the result on `data.curr`. Mirrors steps 1-4 of
119/// `DefaultIterateInitializer::set_initial_iterates`, but with
120/// upstream's warm-start option block governing the push.
121fn seed_from_nlp(data: &IpoptDataHandle, nlp: &Rc<RefCell<dyn IpoptNlp>>, opts: &WarmStartOptions) {
122    let (n_x, n_s, n_yc, n_yd, n_zl, n_zu, n_vl, n_vu) = {
123        let borrow = data.borrow();
124        let c = borrow.curr.as_ref().unwrap();
125        (
126            c.x.dim(),
127            c.s.dim(),
128            c.y_c.dim(),
129            c.y_d.dim(),
130            c.z_l.dim(),
131            c.z_u.dim(),
132            c.v_l.dim(),
133            c.v_u.dim(),
134        )
135    };
136
137    let mut x = DenseVectorSpace::new(n_x).make_new_dense();
138    nlp.borrow_mut().get_starting_x(&mut x);
139    {
140        let nlp_ref = nlp.borrow();
141        push_x_into_interior(
142            &mut x,
143            &*nlp_ref.px_l(),
144            nlp_ref.x_l(),
145            &*nlp_ref.px_u(),
146            nlp_ref.x_u(),
147            opts.bound_push,
148            opts.bound_frac,
149        );
150    }
151
152    let mut s = DenseVectorSpace::new(n_s).make_new_dense();
153    nlp.borrow_mut().eval_d(&x, &mut s);
154    {
155        let nlp_ref = nlp.borrow();
156        push_x_into_interior(
157            &mut s,
158            &*nlp_ref.pd_l(),
159            nlp_ref.d_l(),
160            &*nlp_ref.pd_u(),
161            nlp_ref.d_u(),
162            opts.slack_bound_push,
163            opts.slack_bound_frac,
164        );
165    }
166
167    let mut y_c = DenseVectorSpace::new(n_yc).make_new_dense();
168    let mut y_d = DenseVectorSpace::new(n_yd).make_new_dense();
169    y_c.set(0.0);
170    y_d.set(0.0);
171    nlp.borrow_mut().get_starting_y(&mut y_c, &mut y_d);
172
173    let mut z_l = DenseVectorSpace::new(n_zl).make_new_dense();
174    let mut z_u = DenseVectorSpace::new(n_zu).make_new_dense();
175    let mut v_l = DenseVectorSpace::new(n_vl).make_new_dense();
176    let mut v_u = DenseVectorSpace::new(n_vu).make_new_dense();
177    z_l.set(0.0);
178    z_u.set(0.0);
179    v_l.set(0.0);
180    v_u.set(0.0);
181    nlp.borrow_mut()
182        .get_starting_z(&mut z_l, &mut z_u, &mut v_l, &mut v_u);
183
184    let iv = IteratesVector::new(
185        Rc::new(x),
186        Rc::new(s),
187        Rc::new(y_c),
188        Rc::new(y_d),
189        Rc::new(z_l),
190        Rc::new(z_u),
191        Rc::new(v_l),
192        Rc::new(v_u),
193    );
194    data.borrow_mut().set_curr(iv);
195}
196
197fn is_initialized(v: &Rc<dyn Vector>) -> bool {
198    if v.dim() == 0 {
199        return true;
200    }
201    v.as_any()
202        .downcast_ref::<DenseVector>()
203        .map(|d| d.is_initialized())
204        .unwrap_or(true)
205}
206
207/// Clone `v` into a fresh owned vector and clamp every entry to
208/// `[lo, hi]` componentwise. Empty vectors short-circuit. Vectors that
209/// were never written to (the application's placeholder seed iterates
210/// before any solve ran) collapse to a zero-initialized vector — `0`
211/// is inside every well-formed warm-start clamp range, so this matches
212/// upstream's behavior when a multiplier block has no carry-over
213/// value.
214fn clone_clamped(v: &Rc<dyn Vector>, lo: f64, hi: f64) -> Rc<dyn Vector> {
215    let n = v.dim();
216    if n == 0 {
217        return Rc::clone(v);
218    }
219    let mut out = v.make_new();
220    let initialized = v
221        .as_any()
222        .downcast_ref::<DenseVector>()
223        .map(|d| d.is_initialized())
224        .unwrap_or(true);
225    if initialized {
226        out.copy(&**v);
227    } else {
228        out.set(0.0);
229    }
230    let mut cap_hi = v.make_new();
231    cap_hi.set(hi);
232    out.element_wise_min(&*cap_hi);
233    let mut cap_lo = v.make_new();
234    cap_lo.set(lo);
235    out.element_wise_max(&*cap_lo);
236    Rc::from(out)
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use pounce_linalg::dense_vector::DenseVectorSpace;
243
244    fn dense(n: i32, fill: f64) -> Rc<dyn Vector> {
245        let space = DenseVectorSpace::new(n);
246        let mut v = space.make_new_dense();
247        v.set(fill);
248        Rc::new(v)
249    }
250
251    #[test]
252    fn clamps_multipliers_to_cap() {
253        let v = dense(3, 1e10);
254        let out = clone_clamped(&v, 0.0, 1e6);
255        assert_eq!(out.amax(), 1e6);
256        let v2 = dense(3, -1e10);
257        let out2 = clone_clamped(&v2, -1e6, 1e6);
258        assert_eq!(out2.amax(), 1e6);
259    }
260
261    #[test]
262    fn clamps_bound_mults_nonneg() {
263        let v = dense(3, -5.0);
264        let out = clone_clamped(&v, 0.0, 1e6);
265        assert_eq!(out.amax(), 0.0);
266    }
267
268    #[test]
269    fn empty_vector_short_circuits() {
270        let v = dense(0, 0.0);
271        let out = clone_clamped(&v, 0.0, 1.0);
272        assert_eq!(out.dim(), 0);
273    }
274
275    #[test]
276    fn in_range_values_pass_through_untouched() {
277        let v = dense(3, 0.5);
278        let out = clone_clamped(&v, 0.0, 1.0);
279        assert!((out.max() - 0.5).abs() < 1e-15);
280        assert!((out.min() - 0.5).abs() < 1e-15);
281    }
282
283    #[test]
284    fn uninitialized_source_collapses_to_zero() {
285        // Application's placeholder seed iterate: vector allocated but
286        // never written. `clone_clamped` must fall back to zero instead
287        // of tripping the dense-vector "must be initialized" assert.
288        let space = DenseVectorSpace::new(4);
289        let v: Rc<dyn Vector> = Rc::new(space.make_new_dense());
290        let out = clone_clamped(&v, 0.0, 1e6);
291        assert_eq!(out.amax(), 0.0);
292    }
293}