Skip to main content

pounce_nlp/
tnlp.rs

1//! User-facing `TNLP` trait — port of `Interfaces/IpTNLP.{hpp,cpp}`.
2//!
3//! The Rust shape replaces upstream's two-call `(iRow,jCol,values)`
4//! convention with [`SparsityRequest`], a request enum carrying the
5//! caller-supplied buffers. This is more typesafe (no NULL pointers,
6//! buffer length is type-checked) and matches the eight-method API
7//! upstream documents.
8//!
9//! The `IpoptData` / `IpoptCalculatedQuantities` / `IteratesVector`
10//! parameters of `intermediate_callback` and `finalize_solution` are
11//! introduced as opaque [`IpoptData`] / [`IpoptCq`] types; their full
12//! field set lands in Phase 5.
13//!
14//! Trait objects: `dyn TNLP` is supported. Concrete callers store the
15//! TNLP behind an `Rc<RefCell<dyn TNLP>>` (so eval methods can mutate
16//! internal caches) — `pounce_algorithm::IpoptApplication` handles
17//! wrapping.
18
19use crate::alg_types::SolverReturn;
20use crate::return_codes::AlgorithmMode;
21use pounce_common::types::{Index, Number};
22use std::collections::BTreeMap;
23
24/// Linearity tags. Mirrors `TNLP::LinearityType` upstream.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Linearity {
27    Linear,
28    NonLinear,
29}
30
31/// Index style for triplet I/O. Mirrors `TNLP::IndexStyleEnum`.
32/// `Fortran` (1-based) is what MUMPS / HSL want directly; `C`
33/// (0-based) is more natural for Rust user code.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum IndexStyle {
36    C = 0,
37    Fortran = 1,
38}
39
40/// Problem dimensions returned by [`TNLP::get_nlp_info`].
41#[derive(Debug, Clone, Copy)]
42pub struct NlpInfo {
43    pub n: Index,
44    pub m: Index,
45    pub nnz_jac_g: Index,
46    pub nnz_h_lag: Index,
47    pub index_style: IndexStyle,
48}
49
50/// Variable / constraint metadata buckets, mirroring upstream's
51/// `(StringMetaDataMapType, IntegerMetaDataMapType, NumericMetaDataMapType)`.
52#[derive(Debug, Default, Clone)]
53pub struct MetaData {
54    pub strings: BTreeMap<String, Vec<String>>,
55    pub integers: BTreeMap<String, Vec<Index>>,
56    pub numerics: BTreeMap<String, Vec<Number>>,
57}
58
59/// Conventional [`MetaData::strings`] key for per-index human-readable
60/// names (one entry per variable, or per constraint, in original
61/// problem order). Mirrors upstream Ipopt's `"idx_names"` metadata
62/// key. Carrying names this far lets the debugger report a near-singular
63/// Jacobian row as the `mass_balance` equation instead of "row 3" —
64/// the model-vs-index gap Lee et al. (2024,
65/// <https://doi.org/10.69997/sct.147875>) flag as a key roadblock for
66/// debugging equation-oriented models.
67pub const IDX_NAMES: &str = "idx_names";
68
69/// Bound-data target buffers passed into [`TNLP::get_bounds_info`].
70#[derive(Debug)]
71pub struct BoundsInfo<'a> {
72    pub x_l: &'a mut [Number],
73    pub x_u: &'a mut [Number],
74    pub g_l: &'a mut [Number],
75    pub g_u: &'a mut [Number],
76}
77
78/// Starting-point target buffers passed into [`TNLP::get_starting_point`].
79/// Each `init_*` flag matches upstream — mostly false unless warm-starting.
80#[derive(Debug)]
81pub struct StartingPoint<'a> {
82    pub init_x: bool,
83    pub x: &'a mut [Number],
84    pub init_z: bool,
85    pub z_l: &'a mut [Number],
86    pub z_u: &'a mut [Number],
87    pub init_lambda: bool,
88    pub lambda: &'a mut [Number],
89}
90
91/// Scaling-factor target buffers passed into [`TNLP::get_scaling_parameters`].
92#[derive(Debug)]
93pub struct ScalingRequest<'a> {
94    pub obj_scaling: &'a mut Number,
95    pub use_x_scaling: &'a mut bool,
96    pub x_scaling: &'a mut [Number],
97    pub use_g_scaling: &'a mut bool,
98    pub g_scaling: &'a mut [Number],
99}
100
101/// Mode discriminator for the structure / values calls of
102/// [`TNLP::eval_jac_g`] and [`TNLP::eval_h`]. Replaces upstream's
103/// `iRow != NULL` heuristic.
104#[derive(Debug)]
105pub enum SparsityRequest<'a> {
106    /// First call: fill `irow` and `jcol` with the structure (the
107    /// numbering style is whatever was returned in
108    /// [`NlpInfo::index_style`]). The values array is absent.
109    Structure {
110        irow: &'a mut [Index],
111        jcol: &'a mut [Index],
112    },
113    /// Subsequent calls: fill `values` with the entries of the matrix
114    /// at the current `x` (and, for the Hessian, `lambda`,
115    /// `obj_factor`).
116    Values { values: &'a mut [Number] },
117}
118
119/// Solution as passed to [`TNLP::finalize_solution`].
120#[derive(Debug)]
121pub struct Solution<'a> {
122    pub status: SolverReturn,
123    pub x: &'a [Number],
124    pub z_l: &'a [Number],
125    pub z_u: &'a [Number],
126    pub g: &'a [Number],
127    pub lambda: &'a [Number],
128    pub obj_value: Number,
129}
130
131/// Per-iteration callback payload for [`TNLP::intermediate_callback`].
132#[derive(Debug, Clone, Copy)]
133pub struct IterStats {
134    pub mode: AlgorithmMode,
135    pub iter: Index,
136    pub obj_value: Number,
137    pub inf_pr: Number,
138    pub inf_du: Number,
139    pub mu: Number,
140    pub d_norm: Number,
141    pub regularization_size: Number,
142    pub alpha_du: Number,
143    pub alpha_pr: Number,
144    pub ls_trials: Index,
145}
146
147/// Forward-declared placeholder for `IpoptData`. Phase 5 fills this
148/// in with the full mutable iterate-state structure; for Phase 3 it
149/// is opaque.
150#[derive(Debug, Default)]
151pub struct IpoptData {
152    _private: (),
153}
154
155/// Forward-declared placeholder for `IpoptCalculatedQuantities`.
156/// Phase 5 fills this in.
157#[derive(Debug, Default)]
158pub struct IpoptCq {
159    _private: (),
160}
161
162/// User-facing NLP interface — port of `class TNLP`. Object-safe.
163///
164/// Defaults provided for every method that upstream documents as
165/// "default returns false / does nothing", so simple problems only
166/// override the eight pure-virtual methods.
167pub trait TNLP {
168    /// **Required.** Problem dimensions and triplet index style.
169    fn get_nlp_info(&mut self) -> Option<NlpInfo>;
170
171    /// **Required.** Variable / constraint bounds.
172    fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool;
173
174    /// **Required.** Initial primal (and optionally dual) point.
175    fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool;
176
177    /// **Required.** Objective value at `x`.
178    fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number>;
179
180    /// **Required.** Objective gradient at `x` into `grad_f`.
181    fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool;
182
183    /// **Required.** Constraint values `g(x)`.
184    fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool;
185
186    /// **Required.** Jacobian of `g`. Sparsity vs. values selected by
187    /// `mode`. `x` and `new_x` are unused on the structure call.
188    fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool;
189
190    /// **Required for exact Hessian, optional for L-BFGS.** Hessian
191    /// of the Lagrangian. Default returns false (signals to %Ipopt
192    /// that quasi-Newton must be used).
193    fn eval_h(
194        &mut self,
195        _x: Option<&[Number]>,
196        _new_x: bool,
197        _obj_factor: Number,
198        _lambda: Option<&[Number]>,
199        _new_lambda: bool,
200        _mode: SparsityRequest<'_>,
201    ) -> bool {
202        false
203    }
204
205    /// **Required.** Receives the final iterate after solve.
206    fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq);
207
208    // ---- Optional methods (defaults match upstream's "do nothing") ----
209
210    /// Provide variable/constraint metadata (e.g. `idx_names`).
211    /// Default: no metadata.
212    fn get_var_con_metadata(&mut self, _var: &mut MetaData, _con: &mut MetaData) -> bool {
213        false
214    }
215
216    /// User-supplied scaling, used only when
217    /// `nlp_scaling_method=user-scaling`. Default: declines.
218    fn get_scaling_parameters(&mut self, _req: ScalingRequest<'_>) -> bool {
219        false
220    }
221
222    /// Variable linearity tags (used by Bonmin, not by Ipopt).
223    fn get_variables_linearity(&mut self, _types: &mut [Linearity]) -> bool {
224        false
225    }
226
227    /// Per-variable linearity with respect to the **objective only** (a
228    /// pounce extension; upstream has no objective-scoped query).
229    /// `NonLinear` iff the objective's nonlinear part depends on the
230    /// variable; a variable that enters the objective only linearly (or
231    /// not at all) is `Linear` even when it is nonlinear in a
232    /// constraint. Consumed by presolve's Phase-0 objective-coupling
233    /// guard, which must not mistake constraint-only nonlinearity for
234    /// objective coupling. Default: declines (slice untouched).
235    fn get_objective_variables_linearity(&mut self, _types: &mut [Linearity]) -> bool {
236        false
237    }
238
239    /// Constraint linearity tags. Used by adaptive-mu's
240    /// `nlp_scaling_method=equilibration-based`.
241    fn get_constraints_linearity(&mut self, _types: &mut [Linearity]) -> bool {
242        false
243    }
244
245    /// Number of variables that appear nonlinearly. Returning -1
246    /// means "treat all as nonlinear" (the Ipopt default).
247    fn get_number_of_nonlinear_variables(&mut self) -> Index {
248        -1
249    }
250
251    /// List of nonlinear variable indices, in the index style
252    /// returned from [`Self::get_nlp_info`].
253    fn get_list_of_nonlinear_variables(&mut self, _pos_nonlin_vars: &mut [Index]) -> bool {
254        false
255    }
256
257    /// What this model can *prove* about the constancy of its own
258    /// derivatives (gh #588, phase Q6).
259    ///
260    /// This is the auto-detection behind the four upstream hints
261    /// `grad_f_constant` / `hessian_constant` / `jac_c_constant` /
262    /// `jac_d_constant`. Where a model knows its own algebra — an `.nl`
263    /// body the degree-≤2 recognizer reads exactly — it can establish
264    /// the hint without being told, *and* it can establish that the hint
265    /// is false, which is the case upstream honours anyway and pounce
266    /// refuses.
267    ///
268    /// The default declines: everything
269    /// [`DerivativeProof::Unknown`](crate::constant_derivatives::DerivativeProof::Unknown),
270    /// no rows. That is the truthful answer for a TNLP that offers
271    /// callbacks and no algebra — the C interface, the Python bridge,
272    /// both GAMS links — and it is what makes a user's hint on such a
273    /// model still count: `Unknown` means "not established", never
274    /// "varies", and an asserted hint over `Unknown` is honoured on
275    /// trust. See [`crate::constant_derivatives`] for the full table.
276    ///
277    /// A transparent decorator should forward the inner answer only if
278    /// its transformation preserves both directions. Diagonal variable
279    /// scaling does ([`crate::scaling_tnlp::ScalingTnlp`] forwards);
280    /// presolve does not — it changes what the rows *are* — so a
281    /// presolve wrapper must keep the declining default.
282    fn derivative_proofs(&mut self) -> crate::constant_derivatives::DerivativeProofs {
283        crate::constant_derivatives::DerivativeProofs::default()
284    }
285
286    /// Does [`Self::intermediate_callback`] do anything with its payload?
287    ///
288    /// The algorithm assembles an [`IterStats`] for every fire, and on a
289    /// solve whose callback is the no-op default that is pure overhead:
290    /// `inf_du` assembles ∇L afresh (two `Jᵀy` products plus norms), 4.6% of
291    /// a 53-iteration AC-OPF solve (gh#965). Returning `false` lets the
292    /// algorithm skip the fire, the stats build, and the gh#870 trace-floor
293    /// sink, which only ever feeds this callback back.
294    ///
295    /// The default is `true`, because Rust cannot tell whether an
296    /// implementor overrode `intermediate_callback`: a `false` default would
297    /// silently stop calling every user callback that exists. Return `false`
298    /// only from a TNLP whose callback is the no-op, or provably ignores its
299    /// argument. A decorator that forwards `intermediate_callback` must
300    /// forward this too, or it keeps paying for a callback nobody reads.
301    fn wants_intermediate_callback(&self) -> bool {
302        true
303    }
304
305    /// A model whose functions are coupled to the barrier parameter: called
306    /// by the interior-point loop after every `μ` update with the new `μ`.
307    /// Return `true` when the model's functions changed as a result; the
308    /// loop then discards every evaluation cached at the current point and
309    /// resets the line search (the filter's entries were measured against
310    /// the old functions). The bounds must not change.
311    ///
312    /// The complementarity driver's coupled continuation uses it: the
313    /// Scholtes rows `G·H <= τ` tighten with `μ` inside one solve, rather
314    /// than one solve per `τ` (`MpccTnlp`, `Lowering::Coupled`). The default
315    /// is "nothing moved". A decorator must forward it, or the coupling
316    /// silently stops at that layer.
317    fn update_with_barrier(&mut self, _mu: Number) -> bool {
318        false
319    }
320
321    /// Per-iteration intermediate callback. Returning false requests
322    /// early termination with `User_Requested_Stop`.
323    fn intermediate_callback(
324        &mut self,
325        _stats: IterStats,
326        _ip_data: &IpoptData,
327        _ip_cq: &IpoptCq,
328    ) -> bool {
329        true
330    }
331
332    /// Will [`Self::intermediate_callback`] read the live iterate
333    /// ([`crate::live_iterate::with_current_user_iterate`], the C API's
334    /// `GetIpoptCurrentIterate` / `GetIpoptCurrentViolations`)?
335    ///
336    /// The interior-point arm materialises that iterate before each fire
337    /// only when this says yes, because doing so is not free: the bound
338    /// multipliers of fixed variables are recovered from `∇f + Jᵀλ`
339    /// (one gradient and one Jacobian evaluation), and a presolve layer
340    /// re-evaluates the model to restore its removed rows. Upstream pays
341    /// that on demand inside `get_curr_iterate`; pounce cannot, since the
342    /// callback holds the TNLP those evaluations need.
343    ///
344    /// Default `false`, so a callback that only reads its [`IterStats`]
345    /// (every Python solve fires one) costs nothing extra. The C
346    /// interface answers `true` whenever a callback is registered. A
347    /// decorator that forwards `intermediate_callback` must forward this
348    /// too. The active-set SQP arm installs its iterate unconditionally.
349    fn wants_current_iterate(&self) -> bool {
350        false
351    }
352
353    /// Final metadata pass — called just before
354    /// [`Self::finalize_solution`]. Default does nothing.
355    fn finalize_metadata(&mut self, _var: &MetaData, _con: &MetaData) {}
356
357    /// Whether this TNLP is already an explicit generic-presolve wrapper.
358    ///
359    /// This lets the application preserve the public `wrap_with_presolve`
360    /// workflow when `presolve=yes` is also present in its options. Ordinary
361    /// TNLPs and unrelated decorators return `false`.
362    ///
363    /// A transparent decorator around another TNLP should override this and
364    /// forward `inner.borrow().is_presolve_wrapper()`. Otherwise the public
365    /// solve entry point cannot see a presolve wrapper below the decorator and
366    /// may add a second one.
367    fn is_presolve_wrapper(&self) -> bool {
368        false
369    }
370
371    /// The per-variable scaling factors this decorator applies, if it
372    /// is a scaling wrapper (gh#486). Consumers that read the
373    /// algorithm's iterate rather than the `finalize_solution` payload
374    /// see scaled coordinates and need these to undo the substitution.
375    /// A transparent decorator should forward the inner answer.
376    fn scaling_factors(&self) -> Option<Vec<pounce_common::types::Number>> {
377        None
378    }
379
380    /// A *proof* that this problem has no feasible point, if presolve found
381    /// one. `None` (the default) means "not proved" — which is not the same as
382    /// "feasible".
383    ///
384    /// This is the channel presolve previously lacked. Bound propagation and
385    /// FBBT can both establish emptiness of the feasible region **exactly**,
386    /// but with nowhere to report it they discarded the result and let the IPM
387    /// re-derive a strictly weaker numerical verdict — a stationary point of
388    /// the constraint violation, which for a nonconvex problem proves nothing
389    /// globally. Surfacing the proof lets the solver distinguish *"proved
390    /// infeasible"* from *"converged to a locally infeasible point"*.
391    ///
392    /// A transparent decorator around another TNLP should override this and
393    /// forward `inner.borrow().presolve_infeasibility_proof()`, for the same
394    /// reason [`Self::is_presolve_wrapper`] must be forwarded.
395    fn presolve_infeasibility_proof(&self) -> Option<InfeasibilityProof> {
396        None
397    }
398}
399
400/// How presolve established that the feasible region is empty.
401///
402/// Both variants are *proofs*, not heuristics — see the per-variant notes for
403/// why each is sound in floating point. Anything less than a proof must not be
404/// reported here; the numerical "converged to a locally infeasible point"
405/// verdict has its own path.
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum InfeasibilityProof {
408    /// Linear bound propagation drove some variable's bounds past each other
409    /// (`x_l[j] > x_u[j]`). Over a box, propagating a linear row is exact, and
410    /// the crossing must exceed a `1e-12` margin before it counts — so the
411    /// test errs toward *not* declaring infeasibility.
412    BoundPropagation,
413    /// FBBT interval arithmetic emptied the feasible range of constraint
414    /// `witness`. Sound because every interval operation is **outward
415    /// rounded** (one ULP out on each side), so the computed interval always
416    /// contains the true range: an empty computed interval means the true
417    /// range is empty too.
418    IntervalArithmetic {
419        /// Index of the constraint whose range was proved empty.
420        witness: usize,
421    },
422}
423
424#[cfg(test)]
425mod tests {
426    use super::*;
427
428    /// Tiny `min x[0]^2 + x[1]^2  s.t. x[0] + x[1] = 1` problem.
429    /// Used as a smoke test that the trait is object-safe and the
430    /// defaults compile.
431    struct Mini;
432    impl TNLP for Mini {
433        fn get_nlp_info(&mut self) -> Option<NlpInfo> {
434            Some(NlpInfo {
435                n: 2,
436                m: 1,
437                nnz_jac_g: 2,
438                nnz_h_lag: 2,
439                index_style: IndexStyle::C,
440            })
441        }
442        fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
443            b.x_l.iter_mut().for_each(|v| *v = -1e19);
444            b.x_u.iter_mut().for_each(|v| *v = 1e19);
445            b.g_l[0] = 1.0;
446            b.g_u[0] = 1.0;
447            true
448        }
449        fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
450            assert!(sp.init_x);
451            sp.x[0] = 0.5;
452            sp.x[1] = 0.5;
453            true
454        }
455        fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
456            Some(x[0] * x[0] + x[1] * x[1])
457        }
458        fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
459            grad_f[0] = 2.0 * x[0];
460            grad_f[1] = 2.0 * x[1];
461            true
462        }
463        fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
464            g[0] = x[0] + x[1];
465            true
466        }
467        fn eval_jac_g(
468            &mut self,
469            _x: Option<&[Number]>,
470            _new_x: bool,
471            mode: SparsityRequest<'_>,
472        ) -> bool {
473            match mode {
474                SparsityRequest::Structure { irow, jcol } => {
475                    irow.copy_from_slice(&[0, 0]);
476                    jcol.copy_from_slice(&[0, 1]);
477                }
478                SparsityRequest::Values { values } => {
479                    values.copy_from_slice(&[1.0, 1.0]);
480                }
481            }
482            true
483        }
484        fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
485    }
486
487    #[test]
488    fn tnlp_is_object_safe() {
489        // The trait must be usable behind `dyn`; this also exercises
490        // every default-impl method to make sure they compile.
491        let mut t: Box<dyn TNLP> = Box::new(Mini);
492        let info = t.get_nlp_info().expect("get_nlp_info");
493        assert_eq!(info.n, 2);
494        assert_eq!(info.m, 1);
495        assert_eq!(info.index_style, IndexStyle::C);
496
497        let mut x_l = [0.0; 2];
498        let mut x_u = [0.0; 2];
499        let mut g_l = [0.0; 1];
500        let mut g_u = [0.0; 1];
501        assert!(t.get_bounds_info(BoundsInfo {
502            x_l: &mut x_l,
503            x_u: &mut x_u,
504            g_l: &mut g_l,
505            g_u: &mut g_u
506        }));
507        assert_eq!(g_l[0], 1.0);
508
509        let mut grad = [0.0; 2];
510        assert!(t.eval_grad_f(&[3.0, 4.0], true, &mut grad));
511        assert_eq!(grad, [6.0, 8.0]);
512
513        // exact-Hessian default returns false
514        let mut tmp_v = [0.0; 0];
515        assert!(!t.eval_h(
516            None,
517            false,
518            1.0,
519            None,
520            false,
521            SparsityRequest::Values { values: &mut tmp_v }
522        ));
523
524        // Quasi-Newton info default
525        assert_eq!(t.get_number_of_nonlinear_variables(), -1);
526    }
527
528    #[test]
529    fn sparsity_request_round_trip() {
530        let mut t = Mini;
531        let mut irow = [0; 2];
532        let mut jcol = [0; 2];
533        assert!(t.eval_jac_g(
534            None,
535            false,
536            SparsityRequest::Structure {
537                irow: &mut irow,
538                jcol: &mut jcol
539            }
540        ));
541        assert_eq!(irow, [0, 0]);
542        assert_eq!(jcol, [0, 1]);
543
544        let mut vals = [0.0; 2];
545        assert!(t.eval_jac_g(
546            Some(&[1.0, 2.0]),
547            true,
548            SparsityRequest::Values { values: &mut vals }
549        ));
550        assert_eq!(vals, [1.0, 1.0]);
551    }
552}