Skip to main content

pounce_cli/
counting_tnlp.rs

1//! TNLP wrapper that counts evaluation calls so the CLI can mirror
2//! Ipopt's end-of-run "Number of … evaluations = N" summary block.
3//!
4//! All eight required TNLP methods (and `intermediate_callback`) are
5//! forwarded transparently to the inner TNLP. The counters live in
6//! `Cell<i32>`s on the wrapper itself, so the CLI can read them via
7//! `Rc<RefCell<CountingTnlp>>::borrow()` after the solve completes.
8//!
9//! The wrapper does not count *every* call — calls that pass an
10//! `irow/jcol`-only `SparsityRequest::Structure` (the symbolic
11//! sparsity-pattern call, not the values call) don't represent a real
12//! Jacobian / Hessian evaluation, mirroring the way Ipopt reports
13//! these numbers.
14
15use pounce_common::types::{Index, Number};
16use pounce_nlp::tnlp::{
17    BoundsInfo, InfeasibilityProof, IpoptCq, IpoptData, IterStats, Linearity, MetaData, NlpInfo,
18    ScalingRequest, Solution, SparsityRequest, StartingPoint, TNLP,
19};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22
23pub struct CountingTnlp {
24    inner: Rc<RefCell<dyn TNLP>>,
25    pub n_obj: Cell<i32>,
26    pub n_grad_f: Cell<i32>,
27    pub n_g: Cell<i32>,
28    pub n_jac_g: Cell<i32>,
29    pub n_h: Cell<i32>,
30    /// Primal `x` and constraint duals `lambda` captured at
31    /// `finalize_solution`, in the original-problem space the inner TNLP
32    /// presents. The CLI uses this as a fallback solution source for the
33    /// active-set SQP route, whose solve bypasses the IPM-only
34    /// `on_converged` hook the `.sol` / JSON writers normally read.
35    captured_solution: RefCell<Option<(Vec<Number>, Vec<Number>)>>,
36    /// The `(z_l, z_u)` of that same `finalize_solution`, in the user's
37    /// Ipopt convention (both `>= 0` at an active bound).
38    ///
39    /// Captured for the same reason as `captured_solution` and one more:
40    /// when a losing retry's answer is thrown away and an earlier
41    /// attempt's replayed, `on_converged` has already run for the loser,
42    /// so the CLI's bound-multiplier capture -- the `ipopt_zL_out` /
43    /// `ipopt_zU_out` suffixes -- describes the discarded point. The
44    /// replay is itself a `finalize_solution` call, so this always holds
45    /// the answer being reported. Both sources apply the same
46    /// `Nlp::finalize_solution_z_l` / `_z_u` lift, so they are the same
47    /// numbers and not merely compatible ones.
48    captured_bound_mults: RefCell<Option<(Vec<Number>, Vec<Number>)>>,
49}
50
51impl CountingTnlp {
52    pub fn new(inner: Rc<RefCell<dyn TNLP>>) -> Self {
53        Self {
54            inner,
55            n_obj: Cell::new(0),
56            n_grad_f: Cell::new(0),
57            n_g: Cell::new(0),
58            n_jac_g: Cell::new(0),
59            n_h: Cell::new(0),
60            captured_solution: RefCell::new(None),
61            captured_bound_mults: RefCell::new(None),
62        }
63    }
64
65    /// The `(x, lambda)` captured at the last `finalize_solution`, if any.
66    pub fn captured_solution(&self) -> Option<(Vec<Number>, Vec<Number>)> {
67        self.captured_solution.borrow().clone()
68    }
69
70    /// The `(z_l, z_u)` captured at that same `finalize_solution`.
71    ///
72    /// `None`, and empty vectors, are distinct: a non-`OrigIpoptNlp` model
73    /// hands back empty bound-multiplier blocks and no suffixes are
74    /// written, which callers must not confuse with "not captured".
75    pub fn captured_bound_mults(&self) -> Option<(Vec<Number>, Vec<Number>)> {
76        self.captured_bound_mults.borrow().clone()
77    }
78}
79
80impl TNLP for CountingTnlp {
81    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
82        self.inner.borrow_mut().get_nlp_info()
83    }
84
85    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
86        self.inner.borrow_mut().get_bounds_info(b)
87    }
88
89    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
90        self.inner.borrow_mut().get_starting_point(sp)
91    }
92
93    fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
94        self.n_obj.set(self.n_obj.get() + 1);
95        self.inner.borrow_mut().eval_f(x, new_x)
96    }
97
98    fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
99        self.n_grad_f.set(self.n_grad_f.get() + 1);
100        self.inner.borrow_mut().eval_grad_f(x, new_x, grad_f)
101    }
102
103    fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
104        self.n_g.set(self.n_g.get() + 1);
105        self.inner.borrow_mut().eval_g(x, new_x, g)
106    }
107
108    fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
109        // Only the values call counts as a real Jacobian evaluation;
110        // the symbolic Structure call is bookkeeping.
111        if matches!(mode, SparsityRequest::Values { .. }) {
112            self.n_jac_g.set(self.n_jac_g.get() + 1);
113        }
114        self.inner.borrow_mut().eval_jac_g(x, new_x, mode)
115    }
116
117    fn eval_h(
118        &mut self,
119        x: Option<&[Number]>,
120        new_x: bool,
121        obj_factor: Number,
122        lambda: Option<&[Number]>,
123        new_lambda: bool,
124        mode: SparsityRequest<'_>,
125    ) -> bool {
126        if matches!(mode, SparsityRequest::Values { .. }) {
127            self.n_h.set(self.n_h.get() + 1);
128        }
129        self.inner
130            .borrow_mut()
131            .eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
132    }
133
134    fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq) {
135        *self.captured_solution.borrow_mut() = Some((sol.x.to_vec(), sol.lambda.to_vec()));
136        *self.captured_bound_mults.borrow_mut() = Some((sol.z_l.to_vec(), sol.z_u.to_vec()));
137        self.inner
138            .borrow_mut()
139            .finalize_solution(sol, ip_data, ip_cq);
140    }
141
142    fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
143        self.inner.borrow_mut().get_var_con_metadata(var, con)
144    }
145
146    fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
147        self.inner.borrow_mut().get_scaling_parameters(req)
148    }
149
150    fn get_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
151        self.inner.borrow_mut().get_variables_linearity(types)
152    }
153
154    fn get_objective_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
155        self.inner
156            .borrow_mut()
157            .get_objective_variables_linearity(types)
158    }
159
160    // Without this forward, anything stacked above the counter (the
161    // application's DOF-gate infeasibility probe, a presolve wrapper) sees
162    // the trait default `false` and treats every row as nonlinear, silently
163    // disabling linear-row bound propagation (gh#387).
164    fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
165        self.inner.borrow_mut().get_constraints_linearity(types)
166    }
167
168    fn get_number_of_nonlinear_variables(&mut self) -> Index {
169        self.inner.borrow_mut().get_number_of_nonlinear_variables()
170    }
171
172    /// Transparent decorator: forward the constant-derivative proofs, or
173    /// `OrigIpoptNlp` asks this wrapper — which knows no algebra — and
174    /// gets the declining default, so gh #588 Q6's reuse never engages on
175    /// any model the CLI solves. Neither wrapper changes a row, a
176    /// variable or a derivative's value, so both directions of the proof
177    /// carry through unchanged.
178    fn derivative_proofs(&mut self) -> pounce_nlp::constant_derivatives::DerivativeProofs {
179        self.inner.borrow_mut().derivative_proofs()
180    }
181
182    fn get_list_of_nonlinear_variables(&mut self, pos: &mut [Index]) -> bool {
183        self.inner.borrow_mut().get_list_of_nonlinear_variables(pos)
184    }
185
186    fn intermediate_callback(
187        &mut self,
188        stats: IterStats,
189        ip_data: &IpoptData,
190        ip_cq: &IpoptCq,
191    ) -> bool {
192        self.inner
193            .borrow_mut()
194            .intermediate_callback(stats, ip_data, ip_cq)
195    }
196
197    fn finalize_metadata(&mut self, var: &MetaData, con: &MetaData) {
198        self.inner.borrow_mut().finalize_metadata(var, con)
199    }
200
201    /// Transparent decorator: forward the presolve infeasibility proof, or the
202    /// application never sees it. The CLI stacks this counter *above* the
203    /// presolve wrapper, so without this the proof is swallowed here and the
204    /// solve runs anyway.
205    fn presolve_infeasibility_proof(&self) -> Option<InfeasibilityProof> {
206        self.inner.borrow().presolve_infeasibility_proof()
207    }
208}