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        // The floor's verdict rides BESIDE the raw measurement, never over it,
79        // so a reader can see both which question was asked and how much
80        // negative curvature the floor was able to absorb.
81        if let Some(clearance) = self.curvature_floor {
82            e.insert("curvature_floor_cleared", clearance.cleared.into());
83            put_finite(
84                &mut e,
85                "curvature_interior_min_eigenvalue",
86                clearance.interior_min_eigenvalue,
87            );
88            put_finite(&mut e, "curvature_gradient_floor", clearance.gradient_floor);
89        }
90        e.insert("lambdas_railed_count", self.lambdas_railed.len().into());
91        e.insert("stationary", self.is_stationary().into());
92        e.insert("curvature_admissible", self.curvature_admissible().into());
93        e.insert("summary", self.summary().into());
94        e
95    }
96
97    fn verdict(&self) -> Verdict {
98        if self.certifies() {
99            Verdict::Certified
100        } else {
101            Verdict::Insufficient
102        }
103    }
104}
105
106// ── 2. Sensitivity-coreset error budget ──────────────────────────────────────
107
108impl Certificate for CoresetCertificate {
109    fn claim(&self) -> Claim {
110        Claim::new(
111            "coreset-budget",
112            "the selected row coreset reproduces the full-corpus evidence within \
113             a certified spectral + likelihood error budget; a race decision \
114             inherits the full-corpus verdict only when its margin clears this \
115             budget",
116        )
117    }
118
119    fn evidence(&self) -> Evidence {
120        let mut e = Evidence::new();
121        put_finite(&mut e, "eps_spectral", self.eps_spectral);
122        put_finite(&mut e, "eps_likelihood", self.eps_likelihood);
123        e.insert("dim_effective", self.dim_effective.into());
124        e.insert("n_selected", self.n_selected.into());
125        put_finite(&mut e, "logdet_error_bound", self.logdet_error_bound());
126        put_finite(&mut e, "race_transfer_margin", self.race_transfer_margin());
127        e
128    }
129
130    fn verdict(&self) -> Verdict {
131        // A coreset certificate is a transfer BUDGET, not a standalone decision:
132        // it certifies a race verdict only once a consumer supplies a decision
133        // margin that clears `race_transfer_margin`. With no consumer margin in
134        // hand, the conservative standalone verdict is `Insufficient` (the
135        // budget is present, but nothing has been decided by it yet) when the
136        // budget is finite, and `Unavailable` when it is not.
137        if self.race_transfer_margin().is_finite() {
138            Verdict::Insufficient
139        } else {
140            Verdict::Unavailable
141        }
142    }
143}
144
145/// Map a coreset race outcome (the certificate's own
146/// [`CoresetCertificate::certify_margin`](crate::row_sampling_measure::CoresetCertificate::certify_margin)
147/// rule, evaluated against a consumer's
148/// `decision_margin`) onto the shared [`Verdict`] ladder. This is the
149/// margin-resolved entry point a race consumer uses to obtain a unified verdict
150/// without re-deriving the mapping.
151pub fn coreset_race_verdict(verdict: CoresetMarginVerdict) -> Verdict {
152    match verdict {
153        CoresetMarginVerdict::Certified { .. } => Verdict::Certified,
154        CoresetMarginVerdict::InsufficientMargin { .. } => Verdict::Insufficient,
155    }
156}
157
158/// Verdict for an enclosure resolved against a concrete consumer
159/// `decision_margin`, reusing [`LogdetEnclosure::decide_within_margin`].
160pub fn enclosure_margin_verdict(enclosure: &LogdetEnclosure, decision_margin: f64) -> Verdict {
161    match enclosure.decide_within_margin(decision_margin) {
162        MarginVerdict::Decided { .. } => Verdict::Certified,
163        MarginVerdict::InsufficientMargin { .. } => Verdict::Insufficient,
164    }
165}
166
167// ── 3. Log-det enclosure ─────────────────────────────────────────────────────
168
169impl Certificate for LogdetEnclosure {
170    fn claim(&self) -> Claim {
171        Claim::new(
172            "logdet-enclosure",
173            "the log-determinant is enclosed in a certified [lower, upper] \
174             interval whose midpoint is interchangeable with the exact value for \
175             any decision whose margin exceeds the enclosure gap",
176        )
177    }
178
179    fn evidence(&self) -> Evidence {
180        let mut e = Evidence::new();
181        put_finite(&mut e, "block_diag_logdet", self.block_diag_logdet);
182        put_finite(&mut e, "lower", self.lower);
183        put_finite(&mut e, "upper", self.upper);
184        put_finite(&mut e, "gap", self.gap());
185        put_finite(&mut e, "rho", self.rho);
186        put_finite(&mut e, "p2", self.p2);
187        match self.p3 {
188            Some(p3) => put_finite(&mut e, "p3", p3),
189            None => {
190                e.insert("p3", "n/a".into());
191            }
192        }
193        e
194    }
195
196    fn verdict(&self) -> Verdict {
197        // An enclosure on its own does not certify a decision — only a consumer
198        // margin does (via `decide_within_margin`). The standalone verdict is
199        // `Insufficient` when the enclosure is finite (evidence present, no
200        // decision yet) and `Unavailable` when the bounds are non-finite.
201        if self.lower.is_finite() && self.upper.is_finite() && self.gap().is_finite() {
202            Verdict::Insufficient
203        } else {
204            Verdict::Unavailable
205        }
206    }
207}
208
209// ── 7. Structure-search collapse event ───────────────────────────────────────
210
211impl Certificate for CollapseEvent {
212    fn claim(&self) -> Claim {
213        Claim::new(
214            "structure-collapse",
215            "an atom's active mass fell below the collapse floor during the joint \
216             fit; the guard either reseeded it from a fresh basin or, once the \
217             reseed budget was exhausted, recorded the collapse as the objective's \
218             terminal verdict",
219        )
220    }
221
222    fn evidence(&self) -> Evidence {
223        let mut e = Evidence::new();
224        e.insert("iteration", self.iteration.into());
225        e.insert("atom", self.atom.into());
226        put_finite(&mut e, "max_active_mass", self.max_active_mass);
227        put_finite(&mut e, "floor", self.floor);
228        e.insert(
229            "action",
230            match self.action {
231                CollapseAction::Reseeded => "reseeded",
232                CollapseAction::Terminal => "terminal",
233            }
234            .into(),
235        );
236        e
237    }
238
239    fn verdict(&self) -> Verdict {
240        // A collapse event is, by definition, a guard FIRING — it never certifies
241        // health. A `Reseeded` event is a recovered breach (`Insufficient`: the
242        // breach happened but the fit continued); a `Terminal` event is the
243        // objective's verdict that the collapse stands (`Unavailable`: the claim
244        // of a healthy non-collapsed dictionary cannot be made at all).
245        match self.action {
246            CollapseAction::Reseeded => Verdict::Insufficient,
247            CollapseAction::Terminal => Verdict::Unavailable,
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use gam_problem::topology_certificates::CertificateLedger;
256
257    #[test]
258    fn criterion_stationarity_and_curvature_control_verdict() {
259        let clean = OuterCriterionCertificate {
260            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
261                grad_norm: 1e-8,
262                projected_grad_norm: 1e-8,
263                bound: 1e-6,
264            },
265            hessian_psd: Some(true),
266            lambdas_railed: Vec::new(),
267            curvature_floor: None,
268        };
269        assert_eq!(clean.verdict(), Verdict::Certified);
270        assert!(clean.verdict().is_certified());
271
272        let nonstationary = OuterCriterionCertificate {
273            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
274                grad_norm: 1e-2,
275                projected_grad_norm: 1e-2,
276                bound: 1e-6,
277            },
278            ..clean
279        };
280        assert_eq!(nonstationary.verdict(), Verdict::Insufficient);
281        assert!(!nonstationary.verdict().is_certified());
282        // The claim id is stable and the summary rides the evidence.
283        assert_eq!(nonstationary.claim().id, "outer-optimality");
284        assert!(nonstationary.evidence().contains_key("summary"));
285    }
286
287    #[test]
288    fn coreset_budget_alone_is_insufficient_but_decides_with_margin() {
289        let cert = CoresetCertificate::new(0.1, 0.0, 4, 32).expect("coreset cert");
290        assert_eq!(cert.verdict(), Verdict::Insufficient);
291        // A margin below the budget stays insufficient; above it certifies.
292        let req = cert.race_transfer_margin();
293        assert_eq!(
294            coreset_race_verdict(cert.certify_margin(req * 0.5)),
295            Verdict::Insufficient
296        );
297        assert_eq!(
298            coreset_race_verdict(cert.certify_margin(req * 2.0 + 1.0)),
299            Verdict::Certified
300        );
301    }
302
303    #[test]
304    fn enclosure_certifies_only_when_margin_clears_gap() {
305        let enc = LogdetEnclosure {
306            block_diag_logdet: 10.0,
307            lower: 9.9,
308            upper: 10.1,
309            rho: 0.3,
310            p2: 0.01,
311            p3: None,
312        };
313        assert_eq!(enc.verdict(), Verdict::Insufficient);
314        // gap = 0.2; a margin of 0.5 > gap certifies; 0.1 < gap does not.
315        assert_eq!(enclosure_margin_verdict(&enc, 0.5), Verdict::Certified);
316        assert_eq!(enclosure_margin_verdict(&enc, 0.1), Verdict::Insufficient);
317    }
318
319    #[test]
320    fn collapse_terminal_is_unavailable_reseeded_is_insufficient() {
321        let reseeded = CollapseEvent {
322            iteration: 3,
323            atom: 1,
324            max_active_mass: 1e-4,
325            floor: 1e-3,
326            action: CollapseAction::Reseeded,
327        };
328        assert_eq!(reseeded.verdict(), Verdict::Insufficient);
329        let terminal = CollapseEvent {
330            action: CollapseAction::Terminal,
331            ..reseeded
332        };
333        assert_eq!(terminal.verdict(), Verdict::Unavailable);
334    }
335
336    #[test]
337    fn ledger_rolls_up_to_weakest_member() {
338        let mut ledger = CertificateLedger::new();
339        let clean = OuterCriterionCertificate {
340            stationarity: crate::model_types::OuterStationarityCertificate::AnalyticGradient {
341                grad_norm: 1e-8,
342                projected_grad_norm: 1e-8,
343                bound: 1e-6,
344            },
345            hessian_psd: Some(true),
346            lambdas_railed: Vec::new(),
347            curvature_floor: None,
348        };
349        let cert = CoresetCertificate::new(0.1, 0.0, 4, 32).expect("coreset");
350        ledger.record(&clean); // Certified
351        ledger.record(&cert); // Insufficient
352        assert_eq!(ledger.overall(), Verdict::Insufficient);
353        assert_eq!(ledger.verdict_of("outer-optimality"), Verdict::Certified);
354        assert_eq!(ledger.verdict_of("coreset-budget"), Verdict::Insufficient);
355    }
356}