gam_problem/
estimation_error.rs1use gam_linalg::LinalgError;
2use gam_linalg::faer_ndarray::FaerLinalgError;
3
4use crate::{BasisError, CustomFamilyError, MonotoneRootError};
5
6#[derive(thiserror::Error)]
8pub enum EstimationError {
9 #[error("Underlying basis function generation failed: {0}")]
10 BasisError(#[from] BasisError),
11
12 #[error("Custom-family fit failed: {0}")]
13 CustomFamily(#[from] CustomFamilyError),
14
15 #[error("A linear system solve failed. The penalized Hessian may be singular. Error: {0}")]
16 LinearSystemSolveFailed(FaerLinalgError),
17
18 #[error("Eigendecomposition failed: {0}")]
19 EigendecompositionFailed(FaerLinalgError),
20
21 #[error(
22 "Penalty spectrum check failed in '{context}': non-finite eigenvalue {value:?} at index {index}"
23 )]
24 PenaltySpectrumNonFinite {
25 context: String,
26 index: usize,
27 value: f64,
28 },
29
30 #[error(
31 "Penalty spectrum check failed in '{context}': indefinite eigenvalue {value:.3e} at index {index} (tolerance {tolerance:.3e}, scale {scale:.3e})"
32 )]
33 PenaltySpectrumIndefinite {
34 context: String,
35 index: usize,
36 value: f64,
37 tolerance: f64,
38 scale: f64,
39 },
40
41 #[error("Parameter constraint violation: {0}")]
42 ParameterConstraintViolation(String),
43
44 #[error(
45 "The P-IRLS inner loop did not converge within {max_iterations} iterations. Last gradient norm was {last_change:.6e}."
46 )]
47 PirlsDidNotConverge {
48 max_iterations: usize,
49 last_change: f64,
50 },
51
52 #[error(
53 "Perfect or quasi-perfect separation detected during model fitting at iteration {iteration}. \
54 The model cannot converge because a predictor perfectly separates the binary outcomes. \
55 (Diagnostic: max|eta| = {max_abs_eta:.2e})."
56 )]
57 PerfectSeparationDetected { iteration: usize, max_abs_eta: f64 },
58
59 #[error(
60 "Pre-fit perfect separation detected in the realized binomial inverse-link design: column {column_index} \
61 has a threshold {threshold:.6e} that separates the binary outcomes \
62 (positive_above_threshold={positive_above_threshold}). The unpenalized MLE is not finite; \
63 enable Firth/Jeffreys bias reduction or remove/reparameterize the separating column."
64 )]
65 PrefitPerfectSeparationDetected {
66 column_index: usize,
67 threshold: f64,
68 positive_above_threshold: bool,
69 },
70
71 #[error(
72 "Pre-fit linear separation detected in the realized binomial inverse-link design: \
73 {num_unpenalized_columns} effectively unpenalized columns admit a separating direction \
74 with minimum signed margin {min_signed_margin:.6e} (columns {column_indices:?}). \
75 The unpenalized MLE is not finite; enable Firth/Jeffreys bias reduction or \
76 remove/reparameterize/penalize the separating columns."
77 )]
78 PrefitLinearSeparationDetected {
79 min_signed_margin: f64,
80 num_unpenalized_columns: usize,
81 column_indices: Vec<usize>,
82 },
83
84 #[error(
85 "Pre-fit rank deficiency detected in the realized unpenalized design: rank {rank} < {num_unpenalized_columns} \
86 unpenalized columns (min eigenvalue {min_eigenvalue:.3e}, tolerance {tolerance:.3e}, columns {column_indices:?}). \
87 Remove/reparameterize the aliased columns or add an explicit penalty/constraint before fitting."
88 )]
89 PrefitRankDeficientDesignDetected {
90 rank: usize,
91 num_unpenalized_columns: usize,
92 min_eigenvalue: f64,
93 tolerance: f64,
94 column_indices: Vec<usize>,
95 },
96
97 #[error(
98 "Pre-fit near-degeneracy detected in the realized unpenalized design: the {num_unpenalized_columns} \
99 unpenalized columns span a numerically rank-degenerate direction (Gram condition number {condition_number:.3e} \
100 exceeds tolerance {tolerance:.3e}; min eigenvalue {min_eigenvalue:.3e}, max eigenvalue {max_eigenvalue:.3e}, \
101 columns {column_indices:?}). The unpenalized normal equations are effectively singular along this direction, \
102 so the fit would grind/diverge. Remove/reparameterize the near-aliased columns or add an explicit \
103 penalty/constraint before fitting."
104 )]
105 PrefitNearDegenerateDesignDetected {
106 num_unpenalized_columns: usize,
107 condition_number: f64,
108 min_eigenvalue: f64,
109 max_eigenvalue: f64,
110 tolerance: f64,
111 column_indices: Vec<usize>,
112 },
113
114 #[error(
115 "Perfect or quasi-perfect separation detected during multinomial fitting at iteration {iteration}. \
116 The active class-{active_class_index} logit against the reference class is saturated at training row {row_index}, \
117 so the unpenalized softmax MLE is not finite in that direction. \
118 (Diagnostic: max|eta| = {max_abs_eta:.2e})."
119 )]
120 MultinomialSeparationDetected {
121 iteration: usize,
122 max_abs_eta: f64,
123 active_class_index: usize,
124 row_index: usize,
125 },
126
127 #[error(
128 "Hessian matrix is not positive definite (minimum eigenvalue: {min_eigenvalue:.4e}). This indicates a numerical instability."
129 )]
130 HessianNotPositiveDefinite { min_eigenvalue: f64 },
131
132 #[error("REML smoothing optimization failed to converge: {0}")]
133 RemlOptimizationFailed(String),
134
135 #[error("{context}: unified evaluator returned no gradient in {mode} mode")]
136 GradientUnavailable {
137 context: &'static str,
138 mode: &'static str,
139 },
140
141 #[error("An internal error occurred during model layout or coefficient mapping: {0}")]
142 LayoutError(String),
143
144 #[error(
145 "Model is over-parameterized: {num_coeffs} coefficients for {num_samples} samples.\n\n\
146 Coefficient Breakdown:\n\
147 - Intercept: {intercept_coeffs}\n\
148 - Binary Main Effects: {binary_main_coeffs}\n\
149 - Primary Smooth Effects: {primary_smooth_coeffs}\n\
150 - Binary×Primary Interactions: {binary_primary_interaction_coeffs}\n\
151 - Auxiliary Main Effects: {aux_main_coeffs}\n\
152 - Auxiliary Interactions: {aux_interaction_coeffs}"
153 )]
154 ModelOverparameterized {
155 num_coeffs: usize,
156 num_samples: usize,
157 intercept_coeffs: usize,
158 binary_main_coeffs: usize,
159 primary_smooth_coeffs: usize,
160 aux_main_coeffs: usize,
161 binary_primary_interaction_coeffs: usize,
162 aux_interaction_coeffs: usize,
163 },
164
165 #[error(
166 "Model is ill-conditioned with condition number {condition_number:.2e}. This typically occurs when the model is over-parameterized (too many knots relative to data points). Consider reducing the number of knots or increasing regularization."
167 )]
168 ModelIsIllConditioned { condition_number: f64 },
169
170 #[error("Invalid input: {0}")]
171 InvalidInput(String),
172
173 #[error("monotone root solve: {0}")]
174 MonotoneRoot(#[from] MonotoneRootError),
175
176 #[error("Calibrator training failed: {0}")]
177 CalibratorTrainingFailed(String),
178
179 #[error("Invalid specification: {0}")]
180 InvalidSpecification(String),
181
182 #[error("Prediction error")]
183 PredictionError,
184}
185
186impl core::fmt::Debug for EstimationError {
188 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
189 write!(f, "{}", self)
190 }
191}
192
193impl EstimationError {
194 pub fn is_inner_solve_retreat(&self) -> bool {
205 matches!(
206 self,
207 EstimationError::ModelIsIllConditioned { .. }
208 | EstimationError::PerfectSeparationDetected { .. }
209 | EstimationError::MultinomialSeparationDetected { .. }
210 | EstimationError::PirlsDidNotConverge { .. }
211 )
212 }
213}
214
215impl From<LinalgError> for EstimationError {
216 fn from(error: LinalgError) -> Self {
217 match error {
218 LinalgError::InvalidInput(message) => EstimationError::InvalidInput(message),
219 LinalgError::HessianNotPositiveDefinite { min_eigenvalue } => {
220 EstimationError::HessianNotPositiveDefinite { min_eigenvalue }
221 }
222 LinalgError::ModelIsIllConditioned { condition_number } => {
223 EstimationError::ModelIsIllConditioned { condition_number }
224 }
225 }
226 }
227}