Skip to main content

gam_problem/
basis_error.rs

1//! Leaf error type for basis construction.
2//!
3//! `BasisError` lives in the neutral `gam-problem` crate (not `gam-terms`) so
4//! that downstream consumers — families, design assembly, the terms cluster —
5//! can resolve it without dragging in a `gam-terms` dependency cycle.
6
7use gam_linalg::faer_ndarray::FaerLinalgError;
8use thiserror::Error;
9
10/// A comprehensive error type for all operations within the basis module.
11#[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        /// Pre-formatted constrained-Gram spectrum summary. The structural
87        /// early-return sites bail at the cross-rank check before the Gram is
88        /// ever eigendecomposed, so they report `not computed` rather than a
89        /// misleading NaN; only the spectral-rank-deficiency site fills in real
90        /// max/min eigenvalues and tolerance.
91        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}