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, IpoptCq, IpoptData, IterStats, MetaData, NlpInfo, ScalingRequest, Solution,
18    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}
37
38impl CountingTnlp {
39    pub fn new(inner: Rc<RefCell<dyn TNLP>>) -> Self {
40        Self {
41            inner,
42            n_obj: Cell::new(0),
43            n_grad_f: Cell::new(0),
44            n_g: Cell::new(0),
45            n_jac_g: Cell::new(0),
46            n_h: Cell::new(0),
47            captured_solution: RefCell::new(None),
48        }
49    }
50
51    /// The `(x, lambda)` captured at the last `finalize_solution`, if any.
52    pub fn captured_solution(&self) -> Option<(Vec<Number>, Vec<Number>)> {
53        self.captured_solution.borrow().clone()
54    }
55}
56
57impl TNLP for CountingTnlp {
58    fn get_nlp_info(&mut self) -> Option<NlpInfo> {
59        self.inner.borrow_mut().get_nlp_info()
60    }
61
62    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
63        self.inner.borrow_mut().get_bounds_info(b)
64    }
65
66    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
67        self.inner.borrow_mut().get_starting_point(sp)
68    }
69
70    fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
71        self.n_obj.set(self.n_obj.get() + 1);
72        self.inner.borrow_mut().eval_f(x, new_x)
73    }
74
75    fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
76        self.n_grad_f.set(self.n_grad_f.get() + 1);
77        self.inner.borrow_mut().eval_grad_f(x, new_x, grad_f)
78    }
79
80    fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
81        self.n_g.set(self.n_g.get() + 1);
82        self.inner.borrow_mut().eval_g(x, new_x, g)
83    }
84
85    fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
86        // Only the values call counts as a real Jacobian evaluation;
87        // the symbolic Structure call is bookkeeping.
88        if matches!(mode, SparsityRequest::Values { .. }) {
89            self.n_jac_g.set(self.n_jac_g.get() + 1);
90        }
91        self.inner.borrow_mut().eval_jac_g(x, new_x, mode)
92    }
93
94    fn eval_h(
95        &mut self,
96        x: Option<&[Number]>,
97        new_x: bool,
98        obj_factor: Number,
99        lambda: Option<&[Number]>,
100        new_lambda: bool,
101        mode: SparsityRequest<'_>,
102    ) -> bool {
103        if matches!(mode, SparsityRequest::Values { .. }) {
104            self.n_h.set(self.n_h.get() + 1);
105        }
106        self.inner
107            .borrow_mut()
108            .eval_h(x, new_x, obj_factor, lambda, new_lambda, mode)
109    }
110
111    fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq) {
112        *self.captured_solution.borrow_mut() = Some((sol.x.to_vec(), sol.lambda.to_vec()));
113        self.inner
114            .borrow_mut()
115            .finalize_solution(sol, ip_data, ip_cq);
116    }
117
118    fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
119        self.inner.borrow_mut().get_var_con_metadata(var, con)
120    }
121
122    fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
123        self.inner.borrow_mut().get_scaling_parameters(req)
124    }
125
126    fn get_number_of_nonlinear_variables(&mut self) -> Index {
127        self.inner.borrow_mut().get_number_of_nonlinear_variables()
128    }
129
130    fn get_list_of_nonlinear_variables(&mut self, pos: &mut [Index]) -> bool {
131        self.inner.borrow_mut().get_list_of_nonlinear_variables(pos)
132    }
133
134    fn intermediate_callback(
135        &mut self,
136        stats: IterStats,
137        ip_data: &IpoptData,
138        ip_cq: &IpoptCq,
139    ) -> bool {
140        self.inner
141            .borrow_mut()
142            .intermediate_callback(stats, ip_data, ip_cq)
143    }
144
145    fn finalize_metadata(&mut self, var: &MetaData, con: &MetaData) {
146        self.inner.borrow_mut().finalize_metadata(var, con)
147    }
148}