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}