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}