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