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