Skip to main content

phase0_via_tnlp/
phase0_via_tnlp.rs

1//! End-to-end worked example for PR 8 — Phase 0 orchestrator wired
2//! into `PresolveTnlp`.
3//!
4//! Implements a tiny TNLP whose only constraints are a 2x2 equality
5//! block, builds `PresolveTnlp` with `presolve_auxiliary=yes`, and
6//! prints what the wrapper exposes to the (would-be) IPM. The
7//! orchestrator eliminates the block via clamping `x_l = x_u =
8//! fixed_value` on both variables, so the IPM sees zero remaining
9//! equality rows.
10//!
11//! After that we synthesize a "the IPM finished" call to
12//! `finalize_solution` and confirm the multiplier recovery puts
13//! sensible values at the dropped row indices.
14//!
15//! Run with:
16//! ```bash
17//! cargo run -p pounce-presolve --example phase0_via_tnlp
18//! ```
19
20#![allow(clippy::expect_used, clippy::unwrap_used)]
21
22use std::cell::RefCell;
23use std::rc::Rc;
24
25use pounce_common::types::Number;
26use pounce_nlp::tnlp::{
27    BoundsInfo, IndexStyle, IpoptCq, IpoptData, Linearity, NlpInfo, Solution, SparsityRequest,
28    StartingPoint, TNLP,
29};
30use pounce_nlp::SolverReturn;
31use pounce_presolve::{wrap_with_presolve, AuxiliaryCouplingPolicy, PresolveOptions};
32
33/// `x + y = 3`, `x - y = 1`. Solution (2, 1). Objective: minimise
34/// `(x - 5)^2 + (y - 6)^2` (so the gradient is non-zero at the
35/// optimum after elimination — but here both x and y are forced by
36/// the equalities, so the objective doesn't actually matter for the
37/// solve).
38struct Mini;
39
40impl TNLP for Mini {
41    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
42        Some(NlpInfo {
43            n: 2,
44            m: 2,
45            nnz_jac_g: 4,
46            nnz_h_lag: 2,
47            index_style: IndexStyle::C,
48        })
49    }
50    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
51        for v in b.x_l.iter_mut() {
52            *v = -1e19;
53        }
54        for v in b.x_u.iter_mut() {
55            *v = 1e19;
56        }
57        b.g_l[0] = 3.0;
58        b.g_u[0] = 3.0;
59        b.g_l[1] = 1.0;
60        b.g_u[1] = 1.0;
61        true
62    }
63    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
64        if sp.init_x {
65            sp.x[0] = 0.0;
66            sp.x[1] = 0.0;
67        }
68        true
69    }
70    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
71        Some((x[0] - 5.0).powi(2) + (x[1] - 6.0).powi(2))
72    }
73    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
74        // For PureEquality classification, the gradient must be 0 at
75        // the block variables at the probe point. We return the
76        // *true* gradient — but Phase 0 only inspects the gradient
77        // support at the probe (which is x_probe = (0, 0)). At that
78        // point grad_f = (-10, -12), making the block
79        // ObjectiveCoupled. So we'd need the `Aggressive` policy or a
80        // zero gradient to demonstrate PR 8's elimination on this
81        // exact problem.
82        g[0] = 2.0 * (x[0] - 5.0);
83        g[1] = 2.0 * (x[1] - 6.0);
84        true
85    }
86    fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
87        g[0] = x[0] + x[1];
88        g[1] = x[0] - x[1];
89        true
90    }
91    fn eval_jac_g(
92        &mut self,
93        _x: Option<&[Number]>,
94        _new_x: bool,
95        mode: SparsityRequest<'_>,
96    ) -> bool {
97        match mode {
98            SparsityRequest::Structure { irow, jcol } => {
99                irow.copy_from_slice(&[0, 0, 1, 1]);
100                jcol.copy_from_slice(&[0, 1, 0, 1]);
101            }
102            SparsityRequest::Values { values } => {
103                values.copy_from_slice(&[1.0, 1.0, 1.0, -1.0]);
104            }
105        }
106        true
107    }
108    fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
109        types[0] = Linearity::Linear;
110        types[1] = Linearity::Linear;
111        true
112    }
113    fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
114        println!("\n[finalize_solution received from PresolveTnlp]");
115        println!("   x      = {:?}", sol.x);
116        println!("   lambda = {:?}", sol.lambda);
117        println!("   g      = {:?}", sol.g);
118    }
119}
120
121fn main() {
122    let opts = PresolveOptions {
123        enabled: true,
124        auxiliary: true,
125        // Aggressive lets us eliminate even when grad_f is non-zero
126        // at the block variables at the probe (ObjectiveCoupled).
127        auxiliary_coupling: AuxiliaryCouplingPolicy::Aggressive,
128        ..PresolveOptions::defaults()
129    };
130    let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Mini));
131    let wrapped = wrap_with_presolve(inner, opts).expect("wrap ok");
132
133    let info = wrapped.borrow_mut().get_nlp_info().expect("init");
134    println!("phase0_via_tnlp — PR 8 of pounce#53");
135    println!("====================================");
136    println!("Inner TNLP: n=2, m=2 (two equalities).");
137    println!("After PresolveTnlp wrapping with presolve_auxiliary=yes:");
138    println!("   outer n = {}", info.n);
139    println!("   outer m = {}", info.m);
140    println!("   outer nnz_jac_g = {}", info.nnz_jac_g);
141
142    // Read the clamped bounds.
143    let mut x_l = vec![0.0; info.n as usize];
144    let mut x_u = vec![0.0; info.n as usize];
145    let mut g_l = vec![0.0; info.m as usize];
146    let mut g_u = vec![0.0; info.m as usize];
147    wrapped.borrow_mut().get_bounds_info(BoundsInfo {
148        x_l: &mut x_l,
149        x_u: &mut x_u,
150        g_l: &mut g_l,
151        g_u: &mut g_u,
152    });
153    println!("\nClamped bounds (from PresolveTnlp):");
154    println!("   x_l = {:?}", x_l);
155    println!("   x_u = {:?}", x_u);
156    println!("   g_l = {:?}", g_l);
157
158    // Simulate the IPM handing back a solution: the eliminated vars
159    // are pinned to their clamps; outer lambda is empty (no rows).
160    let sol_x = [x_l[0], x_l[1]];
161    let sol_z_l = vec![0.0; info.n as usize];
162    let sol_z_u = vec![0.0; info.n as usize];
163    let sol_lambda: Vec<Number> = Vec::new(); // outer m == 0
164    let sol_g: Vec<Number> = Vec::new();
165    let ip_data = IpoptData::default();
166    let ip_cq = IpoptCq::default();
167    wrapped.borrow_mut().finalize_solution(
168        Solution {
169            status: SolverReturn::Success,
170            x: &sol_x,
171            z_l: &sol_z_l,
172            z_u: &sol_z_u,
173            g: &sol_g,
174            lambda: &sol_lambda,
175            obj_value: 0.0,
176        },
177        &ip_data,
178        &ip_cq,
179    );
180
181    println!("\n✓ End-to-end orchestrator path exercised.");
182}