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("{0}")]
124 DenseDerivativeMaterializationRefused(String),
125
126 #[error(
127 "Radial basis derivative is undefined at center collision (r = 0) for {kernel} \
128 with dim = {dim}, m = {m}: {message}. The first/second derivative of the \
129 underlying φ(r) does not have a finite limit as r → 0+, so the design-row \
130 gradient and Hessian have no well-defined value at coincident points."
131 )]
132 DegenerateAtCollision {
133 kernel: &'static str,
134 dim: usize,
135 m: f64,
136 message: &'static str,
137 },
138
139 #[error(
140 "Periodic radial basis derivative is undefined at the wrap branch cut \
141 (signed displacement = ±period/2) for raw delta = {raw}, period = {period}: \
142 the wrapped displacement jumps between ±period/2 and the first derivative \
143 w.r.t. the input has a one-sided discontinuity. Move the evaluation point \
144 off the branch cut or define a one-sided convention."
145 )]
146 PeriodicWrapBranchCut { raw: f64, period: f64 },
147
148 #[error("{0}")]
149 Other(String),
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn invalid_degree_mentions_degree_in_message() {
158 let err = BasisError::InvalidDegree(0);
159 let msg = err.to_string();
160 assert!(msg.contains("0"), "expected degree in message, got: {msg}");
161 assert!(msg.to_lowercase().contains("degree"));
162 }
163
164 #[test]
165 fn invalid_range_mentions_start_and_end() {
166 let err = BasisError::InvalidRange(2.5, 1.0);
167 let msg = err.to_string();
168 assert!(msg.contains("2.5") || msg.contains("start"), "message: {msg}");
169 }
170
171 #[test]
172 fn degenerate_range_mentions_zero_width() {
173 let err = BasisError::DegenerateRange(4);
174 let msg = err.to_string().to_lowercase();
175 assert!(msg.contains("zero"), "message: {msg}");
176 }
177
178 #[test]
179 fn invalid_penalty_order_mentions_order_and_num_basis() {
180 let err = BasisError::InvalidPenaltyOrder { order: 5, num_basis: 3 };
181 let msg = err.to_string();
182 assert!(msg.contains("5") && msg.contains("3"), "message: {msg}");
183 }
184
185 #[test]
186 fn insufficient_knots_mentions_degree() {
187 let err = BasisError::InsufficientKnotsForDegree {
188 degree: 3,
189 required: 10,
190 provided: 5,
191 };
192 let msg = err.to_string();
193 assert!(msg.contains("3") && msg.contains("10") && msg.contains("5"), "message: {msg}");
194 }
195
196 #[test]
197 fn invalid_knot_vector_includes_reason() {
198 let err = BasisError::InvalidKnotVector("decreasing knots".to_string());
199 let msg = err.to_string();
200 assert!(msg.contains("decreasing knots"), "message: {msg}");
201 }
202
203 #[test]
204 fn invalid_input_passthrough() {
205 let err = BasisError::InvalidInput("bad value".to_string());
206 assert!(err.to_string().contains("bad value"));
207 }
208
209 #[test]
210 fn other_passthrough() {
211 let err = BasisError::Other("catch-all".to_string());
212 assert_eq!(err.to_string(), "catch-all");
213 }
214}