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