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