Skip to main content

pounce_algorithm/hess/
trait.rs

1//! `HessianUpdater` trait — port of `IpHessianUpdater.hpp`.
2
3use crate::ipopt_cq::IpoptCqHandle;
4use crate::ipopt_data::IpoptDataHandle;
5use std::rc::Rc;
6
7pub trait HessianUpdater {
8    /// Refresh `data.w` for the current iterate. Returns `true` on
9    /// success. Mirrors `IpHessianUpdater::UpdateHessian` (which is
10    /// pure-virtual; implementations write into `IpData().Set_W(...)`).
11    fn update_hessian(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) -> bool;
12
13    /// Whether `data.w` is the *exact* Lagrangian Hessian at the iterate it was
14    /// built from, rather than a quasi-Newton approximation (gh #797).
15    ///
16    /// The negative-curvature probe needs `W` at the iterate it is judging, and
17    /// `data.w` is always one iterate behind at the point the convergence check
18    /// runs. When this is `true` the caller re-evaluates
19    /// [`crate::ipopt_cq::IpoptCalculatedQuantities::curr_exact_hessian`]
20    /// instead — a pure evaluation, where calling `update_hessian` a second
21    /// time at the same iterate would feed the limited-memory updater a
22    /// zero-length curvature pair and count it against
23    /// `limited_memory_max_skipping`.
24    fn provides_exact_hessian(&self) -> bool {
25        false
26    }
27
28    /// `W` at the *current* iterate, for an updater that can produce it as a
29    /// pure function of `(x, y)` with no dependence on the step history.
30    ///
31    /// This exists because `provides_exact_hessian` conflated two different
32    /// questions (gh#823 review, finding 1, reported by @srikanth-gm): "is
33    /// this the exact Hessian?" and "can this be re-evaluated at the current
34    /// iterate?". The negative-curvature probe only ever needed the second.
35    /// Answering it with the first is safe for the limited-memory updater —
36    /// BFGS keeps `B` positive definite, so the probe declines at `δ_x = 0`
37    /// and declining is correct — but it is *not* safe for a finite-difference
38    /// Hessian, which carries genuine negative curvature and was being judged
39    /// one iterate stale. The measured symptom is a stationary maximum
40    /// reported as optimal, where the exact path escapes it.
41    ///
42    /// Returning `None` keeps the previous behaviour: the probe runs against
43    /// whatever `data.w` holds. That is the right answer for a history-carrying
44    /// quasi-Newton `B`, where there is nothing to re-evaluate — recomputing
45    /// would hand the updater a zero-length curvature pair to skip and count
46    /// against `limited_memory_max_skipping`.
47    ///
48    /// An implementation MUST NOT leave `data.w` describing a different
49    /// iterate than it found it describing; the post-optimal sensitivity hook
50    /// reads it.
51    fn hessian_at_current(
52        &mut self,
53        _data: &IpoptDataHandle,
54        _cq: &IpoptCqHandle,
55    ) -> Option<Rc<dyn pounce_linalg::SymMatrix>> {
56        None
57    }
58
59    /// The finite-difference Hessian's pattern and probe census, for the
60    /// updater that has one. `None` for every other updater.
61    ///
62    /// Exposed because the two things that decide whether the mode is
63    /// affordable on a given model — which pattern it ended up with, and
64    /// how many probe groups that cost — were reachable only through the
65    /// `POUNCE_FD_HESSIAN_DEBUG` environment variable, i.e. not from an
66    /// embedder at all. Asked for by @srikanth-gm on gh#823.
67    fn fd_hessian_stats(&self) -> Option<crate::hess::fd_hessian::FdStats> {
68        None
69    }
70
71    /// Discard the accumulated quasi-Newton curvature and re-anchor the
72    /// approximation at the current iterate, returning `true` if there
73    /// was anything to discard (gh#818).
74    ///
75    /// This is the recovery for "the *direction* is unusable", as
76    /// distinct from restoration's "the *point* is infeasible". A
77    /// limited-memory model can carry curvature the iterate has left
78    /// behind, and when the resulting step cannot be accepted at any
79    /// step length the fix is to forget it, not to walk somewhere else.
80    /// L-BFGS-B does exactly this on a line-search failure (`col = 0`
81    /// in `mainlb`); Ipopt has no equivalent, because it always has
82    /// restoration to fall back on — which is fine while the point is
83    /// infeasible and a no-op when it is not.
84    ///
85    /// The default is `false`: an exact Hessian has no history, so
86    /// there is nothing to re-anchor and the caller must fall straight
87    /// through to its existing hand-off.
88    fn reanchor(&mut self) -> bool {
89        false
90    }
91}