Skip to main content

pounce_cli/
builtin.rs

1//! Built-in TNLP test problems for the CLI. Each problem is a
2//! self-contained `impl TNLP` so the CLI can run end-to-end without
3//! parsing an `.nl` file.
4//!
5//! Currently shipped:
6//!
7//! * `quadratic` — `min (x[0]-3)^2 + (x[1]-4)^2`, unconstrained,
8//!   exact Hessian = `2I`. Optimum at `(3, 4)`, `f* = 0`.
9//! * `rosenbrock` — `min 100*(x[1]-x[0]^2)^2 + (1-x[0])^2`,
10//!   unconstrained, exact Hessian. Optimum at `(1, 1)`, `f* = 0`.
11
12use pounce_common::types::{Index, Number};
13use pounce_nlp::tnlp::{
14    BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
15    TNLP,
16};
17use std::cell::RefCell;
18use std::rc::Rc;
19
20pub fn list() -> Vec<&'static str> {
21    vec![
22        "quadratic",
23        "rosenbrock",
24        "bounded-quadratic",
25        "eq-quadratic",
26        "circle",
27    ]
28}
29
30pub fn lookup(name: &str) -> Option<Rc<RefCell<dyn TNLP>>> {
31    match name {
32        "quadratic" => Some(Rc::new(RefCell::new(Quadratic::default()))),
33        "rosenbrock" => Some(Rc::new(RefCell::new(Rosenbrock::default()))),
34        "bounded-quadratic" => Some(Rc::new(RefCell::new(BoundedQuadratic::default()))),
35        "eq-quadratic" => Some(Rc::new(RefCell::new(EqQuadratic::default()))),
36        "circle" => Some(Rc::new(RefCell::new(Circle::default()))),
37        _ => None,
38    }
39}
40
41// --------------------------------------------------------------------
42// Quadratic: min (x0 - 3)^2 + (x1 - 4)^2
43// --------------------------------------------------------------------
44
45#[derive(Debug, Default)]
46pub struct Quadratic {
47    pub final_x: Option<[Number; 2]>,
48    pub final_obj: Number,
49}
50
51impl TNLP for Quadratic {
52    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
53        Some(NlpInfo {
54            n: 2,
55            m: 0,
56            nnz_jac_g: 0,
57            nnz_h_lag: 2, // diagonal Hessian, lower triangle
58            index_style: IndexStyle::C,
59        })
60    }
61
62    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
63        b.x_l.iter_mut().for_each(|v| *v = -2e19);
64        b.x_u.iter_mut().for_each(|v| *v = 2e19);
65        true
66    }
67
68    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
69        sp.x.copy_from_slice(&[0.0, 0.0]);
70        true
71    }
72
73    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
74        Some((x[0] - 3.0).powi(2) + (x[1] - 4.0).powi(2))
75    }
76
77    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
78        grad[0] = 2.0 * (x[0] - 3.0);
79        grad[1] = 2.0 * (x[1] - 4.0);
80        true
81    }
82
83    fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
84        true
85    }
86
87    fn eval_jac_g(
88        &mut self,
89        _x: Option<&[Number]>,
90        _new_x: bool,
91        _mode: SparsityRequest<'_>,
92    ) -> bool {
93        true
94    }
95
96    fn eval_h(
97        &mut self,
98        _x: Option<&[Number]>,
99        _new_x: bool,
100        obj_factor: Number,
101        _lambda: Option<&[Number]>,
102        _new_lambda: bool,
103        mode: SparsityRequest<'_>,
104    ) -> bool {
105        match mode {
106            SparsityRequest::Structure { irow, jcol } => {
107                irow.copy_from_slice(&[0, 1]);
108                jcol.copy_from_slice(&[0, 1]);
109            }
110            SparsityRequest::Values { values } => {
111                values[0] = 2.0 * obj_factor;
112                values[1] = 2.0 * obj_factor;
113            }
114        }
115        true
116    }
117
118    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
119        self.final_x = Some([sol.x[0], sol.x[1]]);
120        self.final_obj = sol.obj_value;
121    }
122}
123
124// --------------------------------------------------------------------
125// Rosenbrock: min 100 (x1 - x0^2)^2 + (1 - x0)^2
126// --------------------------------------------------------------------
127
128#[derive(Debug, Default)]
129pub struct Rosenbrock {
130    pub final_x: Option<[Number; 2]>,
131    pub final_obj: Number,
132}
133
134impl TNLP for Rosenbrock {
135    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
136        Some(NlpInfo {
137            n: 2,
138            m: 0,
139            nnz_jac_g: 0,
140            nnz_h_lag: 3, // dense 2x2 lower triangle: (0,0), (1,0), (1,1)
141            index_style: IndexStyle::C,
142        })
143    }
144
145    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
146        b.x_l.iter_mut().for_each(|v| *v = -2e19);
147        b.x_u.iter_mut().for_each(|v| *v = 2e19);
148        true
149    }
150
151    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
152        sp.x.copy_from_slice(&[-1.2, 1.0]);
153        true
154    }
155
156    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
157        let a = x[1] - x[0] * x[0];
158        let b = 1.0 - x[0];
159        Some(100.0 * a * a + b * b)
160    }
161
162    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
163        // d/dx0 = -400 x0 (x1 - x0^2) - 2 (1 - x0)
164        // d/dx1 =  200 (x1 - x0^2)
165        grad[0] = -400.0 * x[0] * (x[1] - x[0] * x[0]) - 2.0 * (1.0 - x[0]);
166        grad[1] = 200.0 * (x[1] - x[0] * x[0]);
167        true
168    }
169
170    fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
171        true
172    }
173
174    fn eval_jac_g(
175        &mut self,
176        _x: Option<&[Number]>,
177        _new_x: bool,
178        _mode: SparsityRequest<'_>,
179    ) -> bool {
180        true
181    }
182
183    fn eval_h(
184        &mut self,
185        x: Option<&[Number]>,
186        _new_x: bool,
187        obj_factor: Number,
188        _lambda: Option<&[Number]>,
189        _new_lambda: bool,
190        mode: SparsityRequest<'_>,
191    ) -> bool {
192        match mode {
193            SparsityRequest::Structure { irow, jcol } => {
194                // Lower triangle (row >= col): (0,0), (1,0), (1,1).
195                irow.copy_from_slice(&[0, 1, 1]);
196                jcol.copy_from_slice(&[0, 0, 1]);
197            }
198            SparsityRequest::Values { values } => {
199                let x = x.unwrap_or(&[0.0, 0.0]);
200                let h00 = -400.0 * (x[1] - 3.0 * x[0] * x[0]) + 2.0;
201                let h10 = -400.0 * x[0];
202                let h11 = 200.0;
203                values[0] = obj_factor * h00;
204                values[1] = obj_factor * h10;
205                values[2] = obj_factor * h11;
206            }
207        }
208        true
209    }
210
211    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
212        self.final_x = Some([sol.x[0], sol.x[1]]);
213        self.final_obj = sol.obj_value;
214    }
215}
216
217// Small helper for doc references
218#[allow(dead_code)]
219fn _ix(_: Index) {}
220
221// --------------------------------------------------------------------
222// BoundedQuadratic: min (x0-3)^2 + (x1-4)^2 s.t. 0 <= x0 <= 2, 0 <= x1 <= 2
223// Optimum is at the corner (2, 2) with f* = 1 + 4 = 5.
224// --------------------------------------------------------------------
225
226#[derive(Debug, Default)]
227pub struct BoundedQuadratic {
228    pub final_x: Option<[Number; 2]>,
229    pub final_obj: Number,
230}
231
232impl TNLP for BoundedQuadratic {
233    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
234        Some(NlpInfo {
235            n: 2,
236            m: 0,
237            nnz_jac_g: 0,
238            nnz_h_lag: 2,
239            index_style: IndexStyle::C,
240        })
241    }
242
243    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
244        b.x_l.copy_from_slice(&[0.0, 0.0]);
245        b.x_u.copy_from_slice(&[2.0, 2.0]);
246        true
247    }
248
249    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
250        sp.x.copy_from_slice(&[1.0, 1.0]);
251        true
252    }
253
254    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
255        Some((x[0] - 3.0).powi(2) + (x[1] - 4.0).powi(2))
256    }
257
258    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
259        grad[0] = 2.0 * (x[0] - 3.0);
260        grad[1] = 2.0 * (x[1] - 4.0);
261        true
262    }
263
264    fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
265        true
266    }
267
268    fn eval_jac_g(
269        &mut self,
270        _x: Option<&[Number]>,
271        _new_x: bool,
272        _mode: SparsityRequest<'_>,
273    ) -> bool {
274        true
275    }
276
277    fn eval_h(
278        &mut self,
279        _x: Option<&[Number]>,
280        _new_x: bool,
281        obj_factor: Number,
282        _lambda: Option<&[Number]>,
283        _new_lambda: bool,
284        mode: SparsityRequest<'_>,
285    ) -> bool {
286        match mode {
287            SparsityRequest::Structure { irow, jcol } => {
288                irow.copy_from_slice(&[0, 1]);
289                jcol.copy_from_slice(&[0, 1]);
290            }
291            SparsityRequest::Values { values } => {
292                values[0] = 2.0 * obj_factor;
293                values[1] = 2.0 * obj_factor;
294            }
295        }
296        true
297    }
298
299    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
300        self.final_x = Some([sol.x[0], sol.x[1]]);
301        self.final_obj = sol.obj_value;
302    }
303}
304
305// --------------------------------------------------------------------
306// EqQuadratic: min x0^2 + x1^2  s.t.  x0 + x1 = 1
307// Optimum at (1/2, 1/2), f* = 1/2, multiplier y = -1.
308// --------------------------------------------------------------------
309
310#[derive(Debug, Default)]
311pub struct EqQuadratic {
312    pub final_x: Option<[Number; 2]>,
313    pub final_obj: Number,
314}
315
316impl TNLP for EqQuadratic {
317    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
318        Some(NlpInfo {
319            n: 2,
320            m: 1,
321            nnz_jac_g: 2,
322            nnz_h_lag: 2,
323            index_style: IndexStyle::C,
324        })
325    }
326
327    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
328        b.x_l.iter_mut().for_each(|v| *v = -2e19);
329        b.x_u.iter_mut().for_each(|v| *v = 2e19);
330        b.g_l[0] = 1.0;
331        b.g_u[0] = 1.0;
332        true
333    }
334
335    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
336        sp.x.copy_from_slice(&[0.0, 0.0]);
337        true
338    }
339
340    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
341        Some(x[0] * x[0] + x[1] * x[1])
342    }
343
344    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
345        grad[0] = 2.0 * x[0];
346        grad[1] = 2.0 * x[1];
347        true
348    }
349
350    fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
351        g[0] = x[0] + x[1];
352        true
353    }
354
355    fn eval_jac_g(
356        &mut self,
357        _x: Option<&[Number]>,
358        _new_x: bool,
359        mode: SparsityRequest<'_>,
360    ) -> bool {
361        match mode {
362            SparsityRequest::Structure { irow, jcol } => {
363                irow.copy_from_slice(&[0, 0]);
364                jcol.copy_from_slice(&[0, 1]);
365            }
366            SparsityRequest::Values { values } => {
367                values[0] = 1.0;
368                values[1] = 1.0;
369            }
370        }
371        true
372    }
373
374    fn eval_h(
375        &mut self,
376        _x: Option<&[Number]>,
377        _new_x: bool,
378        obj_factor: Number,
379        _lambda: Option<&[Number]>,
380        _new_lambda: bool,
381        mode: SparsityRequest<'_>,
382    ) -> bool {
383        match mode {
384            SparsityRequest::Structure { irow, jcol } => {
385                irow.copy_from_slice(&[0, 1]);
386                jcol.copy_from_slice(&[0, 1]);
387            }
388            SparsityRequest::Values { values } => {
389                values[0] = 2.0 * obj_factor;
390                values[1] = 2.0 * obj_factor;
391            }
392        }
393        true
394    }
395
396    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
397        self.final_x = Some([sol.x[0], sol.x[1]]);
398        self.final_obj = sol.obj_value;
399    }
400}
401
402// --------------------------------------------------------------------
403// Circle: min  x0  s.t.  x0^2 + x1^2 = 1
404// Optimum at (-1, 0), f* = -1, multiplier y = 1/2.
405// Tests nonlinear equality constraint with non-trivial Hessian
406// contribution from the constraint (∇²g_0 = 2I) into the Lagrangian.
407// --------------------------------------------------------------------
408
409#[derive(Debug, Default)]
410pub struct Circle {
411    pub final_x: Option<[Number; 2]>,
412    pub final_obj: Number,
413}
414
415impl TNLP for Circle {
416    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
417        Some(NlpInfo {
418            n: 2,
419            m: 1,
420            nnz_jac_g: 2,
421            nnz_h_lag: 2,
422            index_style: IndexStyle::C,
423        })
424    }
425
426    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
427        b.x_l.iter_mut().for_each(|v| *v = -2e19);
428        b.x_u.iter_mut().for_each(|v| *v = 2e19);
429        b.g_l[0] = 1.0;
430        b.g_u[0] = 1.0;
431        true
432    }
433
434    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
435        sp.x.copy_from_slice(&[-0.5, 0.5]);
436        true
437    }
438
439    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
440        Some(x[0])
441    }
442
443    fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
444        grad[0] = 1.0;
445        grad[1] = 0.0;
446        true
447    }
448
449    fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
450        g[0] = x[0] * x[0] + x[1] * x[1];
451        true
452    }
453
454    fn eval_jac_g(
455        &mut self,
456        x: Option<&[Number]>,
457        _new_x: bool,
458        mode: SparsityRequest<'_>,
459    ) -> bool {
460        match mode {
461            SparsityRequest::Structure { irow, jcol } => {
462                irow.copy_from_slice(&[0, 0]);
463                jcol.copy_from_slice(&[0, 1]);
464            }
465            SparsityRequest::Values { values } => {
466                let x = x.unwrap_or(&[0.0, 0.0]);
467                values[0] = 2.0 * x[0];
468                values[1] = 2.0 * x[1];
469            }
470        }
471        true
472    }
473
474    fn eval_h(
475        &mut self,
476        _x: Option<&[Number]>,
477        _new_x: bool,
478        _obj_factor: Number,
479        lambda: Option<&[Number]>,
480        _new_lambda: bool,
481        mode: SparsityRequest<'_>,
482    ) -> bool {
483        match mode {
484            SparsityRequest::Structure { irow, jcol } => {
485                irow.copy_from_slice(&[0, 1]);
486                jcol.copy_from_slice(&[0, 1]);
487            }
488            SparsityRequest::Values { values } => {
489                // ∇²L = obj_factor * ∇²f + λ * ∇²g_0 = 0 + λ * 2I
490                let lam = lambda.map(|l| l[0]).unwrap_or(0.0);
491                values[0] = 2.0 * lam;
492                values[1] = 2.0 * lam;
493            }
494        }
495        true
496    }
497
498    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
499        self.final_x = Some([sol.x[0], sol.x[1]]);
500        self.final_obj = sol.obj_value;
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn list_contains_known_problems() {
510        let l = list();
511        assert!(l.contains(&"quadratic"));
512        assert!(l.contains(&"rosenbrock"));
513    }
514
515    #[test]
516    fn quadratic_evaluates_correctly() {
517        let mut q = Quadratic::default();
518        let f = q.eval_f(&[3.0, 4.0], true).unwrap();
519        assert_eq!(f, 0.0);
520        let mut g = [0.0; 2];
521        q.eval_grad_f(&[0.0, 0.0], true, &mut g);
522        assert_eq!(g, [-6.0, -8.0]);
523    }
524
525    #[test]
526    fn rosenbrock_grad_zero_at_optimum() {
527        let mut r = Rosenbrock::default();
528        let f = r.eval_f(&[1.0, 1.0], true).unwrap();
529        assert!(f.abs() < 1e-15);
530        let mut g = [0.0; 2];
531        r.eval_grad_f(&[1.0, 1.0], true, &mut g);
532        assert!(g[0].abs() < 1e-12);
533        assert!(g[1].abs() < 1e-12);
534    }
535
536    #[test]
537    fn lookup_returns_known_and_rejects_unknown() {
538        assert!(lookup("quadratic").is_some());
539        assert!(lookup("rosenbrock").is_some());
540        assert!(lookup("nonsense").is_none());
541    }
542}