Skip to main content

rh_orientation_check/
rh_orientation_check.rs

1//! Which orientation does `Solver::compute_reduced_hessian` report?
2//!
3//! Run with:
4//!
5//! ```text
6//! cargo run --release -p pounce-sensitivity --example rh_orientation_check
7//! ```
8//!
9//! gh#937. The pin path returns `−H_R`, and until that issue the sign
10//! was recorded only inside two crossover test files. This example is
11//! the one-command demonstration: it prints the matrix and its spectrum
12//! on a model whose reduced Hessian is known exactly, and scores it
13//! against all three candidates a reader might assume — `+H_R`, `−H_R`
14//! and `H_R⁻¹`. Scoring against three rather than two is what makes it
15//! a check rather than a sign convention restated: `+H_R` and `−H_R`
16//! differ only in sign, but `H_R⁻¹` differs in *magnitude*, so a run
17//! that merely negated would still be caught.
18
19use std::cell::RefCell;
20use std::rc::Rc;
21
22use pounce_algorithm::application::IpoptApplication;
23use pounce_common::types::{Index, Number};
24use pounce_nlp::return_codes::ApplicationReturnStatus;
25use pounce_nlp::tnlp::{
26    BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest, StartingPoint,
27    TNLP,
28};
29use pounce_sensitivity::Solver;
30
31/// `min x0² + x1² + x0·x1`  s.t.  `g0: x0 = p0`, `g1: x1 = p1`.
32///
33/// The objective Hessian is `H = [[2, 1], [1, 2]]`. Both variables are
34/// pinned, so the null space of the active constraints is `{0}` and the
35/// reduced Hessian over the two pin rows is `H` itself — which is the
36/// point: there is no projection to get wrong, so whatever the accessor
37/// returns is `±H` or `H⁻¹` and nothing else.
38struct PinnedQuadratic {
39    p0: Number,
40    p1: Number,
41}
42
43impl TNLP for PinnedQuadratic {
44    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
45        Some(NlpInfo {
46            n: 2,
47            m: 2,
48            nnz_jac_g: 2,
49            nnz_h_lag: 3,
50            index_style: IndexStyle::C,
51        })
52    }
53
54    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
55        for k in 0..2 {
56            b.x_l[k] = -1.0e19;
57            b.x_u[k] = 1.0e19;
58        }
59        b.g_l[0] = self.p0;
60        b.g_u[0] = self.p0;
61        b.g_l[1] = self.p1;
62        b.g_u[1] = self.p1;
63        true
64    }
65
66    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
67        sp.x[0] = self.p0;
68        sp.x[1] = self.p1;
69        true
70    }
71
72    fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
73        Some(x[0] * x[0] + x[1] * x[1] + x[0] * x[1])
74    }
75
76    fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
77        g[0] = 2.0 * x[0] + x[1];
78        g[1] = 2.0 * x[1] + x[0];
79        true
80    }
81
82    fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
83        g[0] = x[0];
84        g[1] = x[1];
85        true
86    }
87
88    fn eval_jac_g(
89        &mut self,
90        _x: Option<&[Number]>,
91        _new_x: bool,
92        mode: SparsityRequest<'_>,
93    ) -> bool {
94        match mode {
95            SparsityRequest::Structure { irow, jcol } => {
96                irow.copy_from_slice(&[0 as Index, 1 as Index]);
97                jcol.copy_from_slice(&[0 as Index, 1 as Index]);
98            }
99            SparsityRequest::Values { values } => values.copy_from_slice(&[1.0, 1.0]),
100        }
101        true
102    }
103
104    fn eval_h(
105        &mut self,
106        _x: Option<&[Number]>,
107        _new_x: bool,
108        obj_factor: Number,
109        _lambda: Option<&[Number]>,
110        _new_lambda: bool,
111        mode: SparsityRequest<'_>,
112    ) -> bool {
113        match mode {
114            SparsityRequest::Structure { irow, jcol } => {
115                // lower triangle of [[2, 1], [1, 2]]
116                irow.copy_from_slice(&[0 as Index, 1 as Index, 1 as Index]);
117                jcol.copy_from_slice(&[0 as Index, 0 as Index, 1 as Index]);
118            }
119            SparsityRequest::Values { values } => {
120                values.copy_from_slice(&[2.0 * obj_factor, obj_factor, 2.0 * obj_factor]);
121            }
122        }
123        true
124    }
125
126    fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
127}
128
129/// `H` itself, column-major, since every variable is pinned.
130const H: [Number; 4] = [2.0, 1.0, 1.0, 2.0];
131
132fn main() {
133    let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(PinnedQuadratic { p0: 1.0, p1: 2.0 }));
134    let mut app = IpoptApplication::new();
135    app.options_mut()
136        .set_integer_value("print_level", 0, true, false)
137        .unwrap();
138    app.options_mut()
139        .set_string_value("sb", "yes", true, false)
140        .unwrap();
141    app.initialize().unwrap();
142
143    let mut solver = Solver::new(app, tnlp);
144    let status = solver.solve();
145    assert!(
146        matches!(
147            status,
148            ApplicationReturnStatus::SolveSucceeded
149                | ApplicationReturnStatus::SolvedToAcceptableLevel
150        ),
151        "solve failed: {status:?}"
152    );
153
154    let (hr, vals, vecs) = solver
155        .compute_reduced_hessian_eigen(&[0, 1], 1.0)
156        .expect("reduced Hessian");
157
158    println!("model:  min x0² + x1² + x0·x1  s.t.  x0 = 1, x1 = 2");
159    println!("H    = [[2, 1], [1, 2]]   (eigenvalues 1 and 3)");
160    println!();
161    println!("compute_reduced_hessian(pins=[0, 1]):");
162    for i in 0..2 {
163        println!("  [{:>9.6}, {:>9.6}]", hr[i], hr[i + 2]);
164    }
165    println!(
166        "  eigenvalues (ascending) = [{:>9.6}, {:>9.6}]",
167        vals[0], vals[1]
168    );
169    for j in 0..2 {
170        println!(
171            "  eigenvector[{j}]          = [{:>9.6}, {:>9.6}]",
172            vecs[2 * j],
173            vecs[2 * j + 1]
174        );
175    }
176    println!();
177
178    // Three candidates, discriminated by magnitude as well as sign:
179    // inv(H) = [[2/3, -1/3], [-1/3, 2/3]].
180    let h_inv: [Number; 4] = [2.0 / 3.0, -1.0 / 3.0, -1.0 / 3.0, 2.0 / 3.0];
181    let candidates: [(&str, [Number; 4]); 3] = [
182        ("+H_R", H),
183        ("-H_R", [-H[0], -H[1], -H[2], -H[3]]),
184        ("H_R⁻¹", h_inv),
185    ];
186    for (name, want) in candidates {
187        let err = (0..4)
188            .map(|k| (hr[k] - want[k]).abs())
189            .fold(0.0 as Number, Number::max);
190        println!(
191            "  vs {name:<6} max|Δ| = {err:.3e}  {}",
192            if err < 1e-7 { "← MATCH" } else { "" }
193        );
194    }
195    println!();
196    println!(
197        "So the ascending spectrum runs STIFFEST first: {:.6} is the curvature-3",
198        vals[0]
199    );
200    println!(
201        "mode and {:.6} the curvature-1 (soft) one — the reverse of the",
202        vals[1]
203    );
204    println!("order a caller reading `+H_R` would assume.");
205}