Skip to main content

gam_linalg/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// Structurally valid ways a diagonal ridge may participate in a computation.
4///
5/// The former public boolean matrix admitted contradictory states such as a
6/// quadratic penalty without the corresponding Hessian. This enum has only
7/// the coherent inhabitants the engine actually selects.
8///
9/// # Why there is no approximate-determinant inhabitant (#2670)
10///
11/// A third variant, `PositivePartApproximateObjective`, used to route the ridged
12/// log-determinant through a smooth positive-part spectral approximation
13/// (`log|A|_reg = Σ log r_ε(σ_j)`), which is a DIFFERENT estimand from the exact
14/// SPD determinant and was named as such. Nothing in production ever selected
15/// it: every construction of it lived under `#[cfg(test)]`, so the only thing it
16/// bought the library was a second, worse answer a user could opt into by
17/// mistake. It is deleted rather than kept as a fallback — a preserved fallback
18/// is a second implementation of the same quantity, and this one changed the
19/// estimand while doing it.
20///
21/// The smooth regulariser itself is NOT deleted and was never this enum's
22/// business: `spectral_regularize` / `spectral_epsilon` stay live in the REML
23/// outer engine's `DenseSpectralOperator`, where a caller genuinely wants the
24/// smooth surrogate together with its matching analytic gradient.
25///
26/// With one determinant semantics left there is no `determinant_mode()` and no
27/// `RidgeDeterminantMode`: a query whose answer is a constant is not a query.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum RidgePolicy {
30    /// Ridge is an explicit part of the exact objective: quadratic, penalty
31    /// normalizer, and Laplace Hessian all include it, using a full SPD logdet.
32    ExactFullObjective,
33    /// Ridge changes only an inner linear solve and never the fitted objective,
34    /// exported Hessian, determinant, covariance, or serialized model.
35    SolverOnly,
36}
37
38impl RidgePolicy {
39    pub const fn exact_full_objective() -> Self {
40        Self::ExactFullObjective
41    }
42
43    pub const fn solver_only() -> Self {
44        Self::SolverOnly
45    }
46
47    #[inline]
48    pub const fn accounts_for_objective(self) -> bool {
49        !matches!(self, Self::SolverOnly)
50    }
51}
52
53#[cfg(test)]
54mod ridge_policy_tests {
55    use super::*;
56
57    #[test]
58    fn exact_policy_accounts_for_the_objective() {
59        assert!(RidgePolicy::exact_full_objective().accounts_for_objective());
60    }
61
62    #[test]
63    fn solver_only_policy_cannot_enter_objective_accounting() {
64        assert!(!RidgePolicy::solver_only().accounts_for_objective());
65    }
66
67    /// #2670 — the inhabitants are exactly the two the engine selects. A third
68    /// would have to be a second answer to the same question, which is what the
69    /// deleted positive-part variant was.
70    ///
71    /// Two checks of the same property, and they fail at different times, which
72    /// is why both are here. The irrefutable `let` is a COMPILE-TIME assertion:
73    /// re-adding a variant makes the pattern refutable and this file stops
74    /// compiling. The runtime assertions below say the constructors between them
75    /// still reach BOTH inhabitants and reach each exactly once — a compile-time
76    /// check cannot see a constructor that was quietly re-pointed at its
77    /// sibling, and the enum would still have two inhabitants while the engine
78    /// could only select one (#2818: a test that reaches no runtime assertion
79    /// passes for every behaviour of the code it calls).
80    #[test]
81    fn the_policy_has_no_third_inhabitant() {
82        let constructed = [
83            RidgePolicy::exact_full_objective(),
84            RidgePolicy::solver_only(),
85        ];
86        let mut exact = 0usize;
87        let mut solver = 0usize;
88        for policy in constructed {
89            let (RidgePolicy::ExactFullObjective | RidgePolicy::SolverOnly) = policy;
90            match policy {
91                RidgePolicy::ExactFullObjective => exact += 1,
92                RidgePolicy::SolverOnly => solver += 1,
93            }
94        }
95        assert_eq!(
96            (exact, solver),
97            (1, 1),
98            "the two constructors must reach the two distinct inhabitants, once each"
99        );
100        assert_ne!(
101            constructed[0], constructed[1],
102            "a constructor re-pointed at its sibling leaves the enum's arity intact \
103             and the engine with one selectable policy"
104        );
105    }
106}