gam_problem/
basis_error.rs1use gam_linalg::faer_ndarray::FaerLinalgError;
8use thiserror::Error;
9
10#[derive(Error, Debug)]
12pub enum BasisError {
13 #[error("Spline degree must be at least 1, but was {0}.")]
14 InvalidDegree(usize),
15
16 #[error(
17 "Spline degree {degree} is too low for derivative order {derivative_order}; need degree >= {minimum_degree}."
18 )]
19 InsufficientDegreeForDerivative {
20 degree: usize,
21 derivative_order: usize,
22 minimum_degree: usize,
23 },
24
25 #[error("Data range is invalid: start ({0}) must be less than or equal to end ({1}).")]
26 InvalidRange(f64, f64),
27
28 #[error(
29 "Data range has zero width (min equals max), which collapses the B-spline knot domain; requested {0} internal knots."
30 )]
31 DegenerateRange(usize),
32
33 #[error(
34 "Penalty order ({order}) must be positive and less than the number of basis functions ({num_basis})."
35 )]
36 InvalidPenaltyOrder { order: usize, num_basis: usize },
37
38 #[error(
39 "Insufficient knots for degree {degree} spline: need at least {required} knots but only {provided} were provided."
40 )]
41 InsufficientKnotsForDegree {
42 degree: usize,
43 required: usize,
44 provided: usize,
45 },
46
47 #[error(
48 "Cannot apply sum-to-zero constraint: requires at least 2 basis functions, but only {found} were provided."
49 )]
50 InsufficientColumnsForConstraint { found: usize },
51
52 #[error(
53 "Constraint matrix must have the same number of rows as the basis: basis has {basisrows}, constraint has {constraintrows}."
54 )]
55 ConstraintMatrixRowMismatch {
56 basisrows: usize,
57 constraintrows: usize,
58 },
59
60 #[error(
61 "Weights dimension mismatch: expected {expected} weights to match basis matrix rows, but got {found}."
62 )]
63 WeightsDimensionMismatch { expected: usize, found: usize },
64
65 #[error("QR decomposition failed while applying constraints: {0}")]
66 LinalgError(#[from] FaerLinalgError),
67
68 #[error(
69 "Failed to identify a constraint nullspace basis at {site}: \
70 coefficient dim {coeff_dim}, cross-rank {cross_rank}, \
71 constraint Frobenius {cross_frobenius:.3e}, \
72 constrained Gram spectrum {gram_spectrum}. \
73 The smooth basis collapses onto the parametric block — typical causes: \
74 (a) the smooth's evaluated kernel underflows after projecting out the \
75 polynomial nullspace, leaving only floating-point noise (Duchon hybrid \
76 in moderate-to-high d with length_scale near pairwise center distances); \
77 (b) the parametric block already spans the smooth's column space \
78 (over-restrictive identifiability constraint); \
79 (c) the smooth has effective rank ≤ parametric-block size on this data."
80 )]
81 ConstraintNullspaceCollapsed {
82 site: &'static str,
83 cross_rank: usize,
84 coeff_dim: usize,
85 cross_frobenius: f64,
86 gram_spectrum: String,
92 },
93
94 #[error(
95 "Knot vector is degenerate: all Greville abscissae are equal, so linear constraint cannot be applied."
96 )]
97 DegenerateKnots,
98
99 #[error(
100 "The provided knot vector is invalid: {0}. It must be non-decreasing and contain only finite values."
101 )]
102 InvalidKnotVector(String),
103
104 #[error("Failed to build sparse basis matrix: {0}")]
105 SparseCreation(String),
106
107 #[error("Dimension mismatch: {0}")]
108 DimensionMismatch(String),
109
110 #[error(
111 "Indefinite penalty matrix in {context}: minimum eigenvalue {min_eigenvalue:.3e} is below tolerance {tolerance:.3e}. {guidance}"
112 )]
113 IndefinitePenalty {
114 context: String,
115 min_eigenvalue: f64,
116 tolerance: f64,
117 guidance: String,
118 },
119
120 #[error("Invalid input: {0}")]
121 InvalidInput(String),
122
123 #[error(
124 "Radial basis derivative is undefined at center collision (r = 0) for {kernel} \
125 with dim = {dim}, m = {m}: {message}. The first/second derivative of the \
126 underlying φ(r) does not have a finite limit as r → 0+, so the design-row \
127 gradient and Hessian have no well-defined value at coincident points."
128 )]
129 DegenerateAtCollision {
130 kernel: &'static str,
131 dim: usize,
132 m: f64,
133 message: &'static str,
134 },
135
136 #[error("{0}")]
137 Other(String),
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn invalid_degree_mentions_degree_in_message() {
146 let err = BasisError::InvalidDegree(0);
147 let msg = err.to_string();
148 assert!(msg.contains("0"), "expected degree in message, got: {msg}");
149 assert!(msg.to_lowercase().contains("degree"));
150 }
151
152 #[test]
153 fn invalid_range_mentions_start_and_end() {
154 let err = BasisError::InvalidRange(2.5, 1.0);
155 let msg = err.to_string();
156 assert!(
157 msg.contains("2.5") || msg.contains("start"),
158 "message: {msg}"
159 );
160 }
161
162 #[test]
163 fn degenerate_range_mentions_zero_width() {
164 let err = BasisError::DegenerateRange(4);
165 let msg = err.to_string().to_lowercase();
166 assert!(msg.contains("zero"), "message: {msg}");
167 }
168
169 #[test]
170 fn invalid_penalty_order_mentions_order_and_num_basis() {
171 let err = BasisError::InvalidPenaltyOrder {
172 order: 5,
173 num_basis: 3,
174 };
175 let msg = err.to_string();
176 assert!(msg.contains("5") && msg.contains("3"), "message: {msg}");
177 }
178
179 #[test]
180 fn insufficient_knots_mentions_degree() {
181 let err = BasisError::InsufficientKnotsForDegree {
182 degree: 3,
183 required: 10,
184 provided: 5,
185 };
186 let msg = err.to_string();
187 assert!(
188 msg.contains("3") && msg.contains("10") && msg.contains("5"),
189 "message: {msg}"
190 );
191 }
192
193 #[test]
194 fn invalid_knot_vector_includes_reason() {
195 let err = BasisError::InvalidKnotVector("decreasing knots".to_string());
196 let msg = err.to_string();
197 assert!(msg.contains("decreasing knots"), "message: {msg}");
198 }
199
200 #[test]
201 fn invalid_input_passthrough() {
202 let err = BasisError::InvalidInput("bad value".to_string());
203 assert!(err.to_string().contains("bad value"));
204 }
205
206 #[test]
207 fn other_passthrough() {
208 let err = BasisError::Other("catch-all".to_string());
209 assert_eq!(err.to_string(), "catch-all");
210 }
211}