Skip to main content

gam_solve/inference/
certificate_impls.rs

1//! [`Certificate`] implementations and margin-resolved [`Verdict`] mappings for
2//! the gam-solve-tier certificate zoo (task #16; descended #1521).
3//!
4//! Two concerns live here, both gam-solve-tier: (a) the `impl Certificate for …`
5//! blocks for the gam-solve-owned certificate types ([`OuterCriterionCertificate`],
6//! [`CoresetCertificate`](crate::row_sampling_measure::CoresetCertificate),
7//! [`LogdetEnclosure`], [`CollapseEvent`](crate::structure_search::CollapseEvent)),
8//! and (b) the two pure margin-resolution helpers, whose only inputs are
9//! gam-solve-tier types ([`LogdetEnclosure`]/[`MarginVerdict`] and
10//! [`CoresetMarginVerdict`](crate::row_sampling_measure::CoresetMarginVerdict))
11//! plus the contracted-down [`Verdict`] ladder. Both were relocated out of the
12//! monolith root (`gam::inference::certificate_impls`) to satisfy the coherence
13//! orphan rule: the [`Certificate`] trait now lives in the neutral `gam-problem`
14//! crate and these types are owned here in `gam-solve`, so the impls must be
15//! defined in the type's home crate. The bodies are byte-identical to the
16//! monolith originals, so there remains exactly one decision rule per verdict.
17//! (The gam-sae-owned certificate types — `EncodeResult`, `ResidualGaugeReport`,
18//! `CertificateInputs` — carry their own impls in `gam_sae::certificate_impls`.)
19
20use crate::logdet_bounds::{LogdetEnclosure, MarginVerdict};
21use crate::model_types::OuterCriterionCertificate;
22use crate::row_sampling_measure::{CoresetCertificate, CoresetMarginVerdict};
23use crate::structure_search::{CollapseAction, CollapseEvent};
24use gam_problem::topology_certificates::{Certificate, Claim, Evidence, Verdict};
25
26/// Helper: insert a scalar only when finite, else record it as text "n/a" so the
27/// evidence is explicit about a missing quantity (never a silent 0.0).
28fn put_finite(evidence: &mut Evidence, key: &'static str, value: f64) {
29    if value.is_finite() {
30        evidence.insert(key, value.into());
31    } else {
32        evidence.insert(key, "n/a".into());
33    }
34}
35
36// ── 1. Outer-optimum first-order self-audit (#931/#934) ──────────────────────
37
38impl Certificate for OuterCriterionCertificate {
39    fn claim(&self) -> Claim {
40        Claim::new(
41            "outer-optimality",
42            concat!(
43                "the returned outer optimum is analytically KKT-stationary and ",
44                "its available exact curvature is not indefinite",
45            ),
46        )
47    }
48
49    fn evidence(&self) -> Evidence {
50        let mut e = Evidence::new();
51        put_finite(
52            &mut e,
53            "stationarity_raw_norm",
54            self.stationarity.raw_norm(),
55        );
56        put_finite(
57            &mut e,
58            "stationarity_projected_norm",
59            self.stationarity.projected_norm(),
60        );
61        put_finite(&mut e, "stationarity_bound", self.stationarity.bound());
62        e.insert(
63            "stationarity_kind",
64            if self.stationarity.is_fixed_point() {
65                "fixed_point"
66            } else {
67                "analytic_gradient"
68            }
69            .into(),
70        );
71        e.insert(
72            "hessian_psd",
73            match self.hessian_psd {
74                Some(psd) => psd.into(),
75                None => "n/a".into(),
76            },
77        );
78        e.insert("lambdas_railed_count", self.lambdas_railed.len().into());
79        e.insert("stationary", self.is_stationary().into());
80        e.insert("curvature_admissible", self.curvature_admissible().into());
81        e.insert("summary", self.summary().into());
82        e
83    }
84
85    fn verdict(&self) -> Verdict {
86        if self.certifies() {
87            Verdict::Certified
88        } else {
89            Verdict::Insufficient
90        }
91    }
92}
93
94// ── 2. Sensitivity-coreset error budget ──────────────────────────────────────
95
96impl Certificate for CoresetCertificate {
97    fn claim(&self) -> Claim {
98        Claim::new(
99            "coreset-budget",
100            "the selected row coreset reproduces the full-corpus evidence within \
101             a certified spectral + likelihood error budget; a race decision \
102             inherits the full-corpus verdict only when its margin clears this \
103             budget",
104        )
105    }
106
107    fn evidence(&self) -> Evidence {
108        let mut e = Evidence::new();
109        put_finite(&mut e, "eps_spectral", self.eps_spectral);
110        put_finite(&mut e, "eps_likelihood", self.eps_likelihood);
111        e.insert("dim_effective", self.dim_effective.into());
112        e.insert("n_selected", self.n_selected.into());
113        put_finite(&mut e, "logdet_error_bound", self.logdet_error_bound());
114        put_finite(&mut e, "race_transfer_margin", self.race_transfer_margin());
115        e
116    }
117
118    fn verdict(&self) -> Verdict {
119        // A coreset certificate is a transfer BUDGET, not a standalone decision:
120        // it certifies a race verdict only once a consumer supplies a decision
121        // margin that clears `race_transfer_margin`. With no consumer margin in
122        // hand, the conservative standalone verdict is `Insufficient` (the
123        // budget is present, but nothing has been decided by it yet) when the
124        // budget is finite, and `Unavailable` when it is not.
125        if self.race_transfer_margin().is_finite() {
126            Verdict::Insufficient
127        } else {
128            Verdict::Unavailable
129        }
130    }
131}
132
133/// Map a coreset race outcome (the certificate's own
134/// [`CoresetCertificate::certify_margin`](crate::row_sampling_measure::CoresetCertificate::certify_margin)
135/// rule, evaluated against a consumer's
136/// `decision_margin`) onto the shared [`Verdict`] ladder. This is the
137/// margin-resolved entry point a race consumer uses to obtain a unified verdict
138/// without re-deriving the mapping.
139pub fn coreset_race_verdict(verdict: CoresetMarginVerdict) -> Verdict {
140    match verdict {
141        CoresetMarginVerdict::Certified { .. } => Verdict::Certified,
142        CoresetMarginVerdict::InsufficientMargin { .. } => Verdict::Insufficient,
143    }
144}
145
146/// Verdict for an enclosure resolved against a concrete consumer
147/// `decision_margin`, reusing [`LogdetEnclosure::decide_within_margin`].
148pub fn enclosure_margin_verdict(enclosure: &LogdetEnclosure, decision_margin: f64) -> Verdict {
149    match enclosure.decide_within_margin(decision_margin) {
150        MarginVerdict::Decided { .. } => Verdict::Certified,
151        MarginVerdict::InsufficientMargin { .. } => Verdict::Insufficient,
152    }
153}
154
155// ── 3. Log-det enclosure ─────────────────────────────────────────────────────
156
157impl Certificate for LogdetEnclosure {
158    fn claim(&self) -> Claim {
159        Claim::new(
160            "logdet-enclosure",
161            "the log-determinant is enclosed in a certified [lower, upper] \
162             interval whose midpoint is interchangeable with the exact value for \
163             any decision whose margin exceeds the enclosure gap",
164        )
165    }
166
167    fn evidence(&self) -> Evidence {
168        let mut e = Evidence::new();
169        put_finite(&mut e, "block_diag_logdet", self.block_diag_logdet);
170        put_finite(&mut e, "lower", self.lower);
171        put_finite(&mut e, "upper", self.upper);
172        put_finite(&mut e, "gap", self.gap());
173        put_finite(&mut e, "rho", self.rho);
174        put_finite(&mut e, "p2", self.p2);
175        match self.p3 {
176            Some(p3) => put_finite(&mut e, "p3", p3),
177            None => {
178                e.insert("p3", "n/a".into());
179            }
180        }
181        e
182    }
183
184    fn verdict(&self) -> Verdict {
185        // An enclosure on its own does not certify a decision — only a consumer
186        // margin does (via `decide_within_margin`). The standalone verdict is
187        // `Insufficient` when the enclosure is finite (evidence present, no
188        // decision yet) and `Unavailable` when the bounds are non-finite.
189        if self.lower.is_finite() && self.upper.is_finite() && self.gap().is_finite() {
190            Verdict::Insufficient
191        } else {
192            Verdict::Unavailable
193        }
194    }
195}
196
197// ── 7. Structure-search collapse event ───────────────────────────────────────
198
199impl Certificate for CollapseEvent {
200    fn claim(&self) -> Claim {
201        Claim::new(
202            "structure-collapse",
203            "an atom's active mass fell below the collapse floor during the joint \
204             fit; the guard either reseeded it from a fresh basin or, once the \
205             reseed budget was exhausted, recorded the collapse as the objective's \
206             terminal verdict",
207        )
208    }
209
210    fn evidence(&self) -> Evidence {
211        let mut e = Evidence::new();
212        e.insert("iteration", self.iteration.into());
213        e.insert("atom", self.atom.into());
214        put_finite(&mut e, "max_active_mass", self.max_active_mass);
215        put_finite(&mut e, "floor", self.floor);
216        e.insert(
217            "action",
218            match self.action {
219                CollapseAction::Reseeded => "reseeded",
220                CollapseAction::Terminal => "terminal",
221            }
222            .into(),
223        );
224        e
225    }
226
227    fn verdict(&self) -> Verdict {
228        // A collapse event is, by definition, a guard FIRING — it never certifies
229        // health. A `Reseeded` event is a recovered breach (`Insufficient`: the
230        // breach happened but the fit continued); a `Terminal` event is the
231        // objective's verdict that the collapse stands (`Unavailable`: the claim
232        // of a healthy non-collapsed dictionary cannot be made at all).
233        match self.action {
234            CollapseAction::Reseeded => Verdict::Insufficient,
235            CollapseAction::Terminal => Verdict::Unavailable,
236        }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use gam_problem::topology_certificates::CertificateLedger;
244
245    #[test]
246    fn criterion_stationarity_and_curvature_control_verdict() {
247        let clean = OuterCriterionCertificate {
248            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
249                grad_norm: 1e-8,
250                projected_grad_norm: 1e-8,
251                bound: 1e-6,
252            },
253            hessian_psd: Some(true),
254            lambdas_railed: Vec::new(),
255        };
256        assert_eq!(clean.verdict(), Verdict::Certified);
257        assert!(clean.verdict().is_certified());
258
259        let nonstationary = OuterCriterionCertificate {
260            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
261                grad_norm: 1e-2,
262                projected_grad_norm: 1e-2,
263                bound: 1e-6,
264            },
265            ..clean
266        };
267        assert_eq!(nonstationary.verdict(), Verdict::Insufficient);
268        assert!(!nonstationary.verdict().is_certified());
269        // The claim id is stable and the summary rides the evidence.
270        assert_eq!(nonstationary.claim().id, "outer-optimality");
271        assert!(nonstationary.evidence().contains_key("summary"));
272    }
273
274    #[test]
275    fn coreset_budget_alone_is_insufficient_but_decides_with_margin() {
276        let cert = CoresetCertificate::new(0.1, 0.0, 4, 32).expect("coreset cert");
277        assert_eq!(cert.verdict(), Verdict::Insufficient);
278        // A margin below the budget stays insufficient; above it certifies.
279        let req = cert.race_transfer_margin();
280        assert_eq!(
281            coreset_race_verdict(cert.certify_margin(req * 0.5)),
282            Verdict::Insufficient
283        );
284        assert_eq!(
285            coreset_race_verdict(cert.certify_margin(req * 2.0 + 1.0)),
286            Verdict::Certified
287        );
288    }
289
290    #[test]
291    fn enclosure_certifies_only_when_margin_clears_gap() {
292        let enc = LogdetEnclosure {
293            block_diag_logdet: 10.0,
294            lower: 9.9,
295            upper: 10.1,
296            rho: 0.3,
297            p2: 0.01,
298            p3: None,
299        };
300        assert_eq!(enc.verdict(), Verdict::Insufficient);
301        // gap = 0.2; a margin of 0.5 > gap certifies; 0.1 < gap does not.
302        assert_eq!(enclosure_margin_verdict(&enc, 0.5), Verdict::Certified);
303        assert_eq!(enclosure_margin_verdict(&enc, 0.1), Verdict::Insufficient);
304    }
305
306    #[test]
307    fn collapse_terminal_is_unavailable_reseeded_is_insufficient() {
308        let reseeded = CollapseEvent {
309            iteration: 3,
310            atom: 1,
311            max_active_mass: 1e-4,
312            floor: 1e-3,
313            action: CollapseAction::Reseeded,
314        };
315        assert_eq!(reseeded.verdict(), Verdict::Insufficient);
316        let terminal = CollapseEvent {
317            action: CollapseAction::Terminal,
318            ..reseeded
319        };
320        assert_eq!(terminal.verdict(), Verdict::Unavailable);
321    }
322
323    #[test]
324    fn ledger_rolls_up_to_weakest_member() {
325        let mut ledger = CertificateLedger::new();
326        let clean = OuterCriterionCertificate {
327            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
328                grad_norm: 1e-8,
329                projected_grad_norm: 1e-8,
330                bound: 1e-6,
331            },
332            hessian_psd: Some(true),
333            lambdas_railed: Vec::new(),
334        };
335        let cert = CoresetCertificate::new(0.1, 0.0, 4, 32).expect("coreset");
336        ledger.record(&clean); // Certified
337        ledger.record(&cert); // Insufficient
338        assert_eq!(ledger.overall(), Verdict::Insufficient);
339        assert_eq!(ledger.verdict_of("outer-optimality"), Verdict::Certified);
340        assert_eq!(ledger.verdict_of("coreset-budget"), Verdict::Insufficient);
341    }
342}