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