bixverse_core/errors.rs
1//! Errors raised by `bixverse-core`.
2
3use crate::core::math::sparse::CompressedSparseFormat;
4use ann_search_rs::utils::dist::Dist;
5use thiserror::Error;
6
7/// Error variants raised by `bixverse-core`.
8///
9/// Grouped by subsystem.
10#[derive(Debug, Error)]
11pub enum CoreErrors {
12 // -- arguments --
13 /// General error for invalid arguments to a given function
14 #[error("Invalid argument: {0}")]
15 InvalidArgument(String),
16
17 /// Error for wrong integer parameters
18 #[error("Parameter {0} must be a positive non-zero integer.")]
19 MustBePositive(String),
20
21 // -- Math / Faer --
22 /// SVD from faer failed to converge or returned no solution.
23 ///
24 /// Typically caused by ill-conditioned or degenerate input (e.g. all-zero
25 /// rows, NaNs, rank-deficient matrices beyond the requested rank).
26 #[error("The faer SVD failed: {0}")]
27 FaerSvdError(String),
28
29 /// Eigen decomposition from faer failed.
30 ///
31 /// Usually indicates a non-symmetric matrix being passed to a symmetric
32 /// solver, or numerical breakdown on degenerate input.
33 #[error("The faer Eigen decomposition failed - please verify the data")]
34 FaerEigenError,
35
36 /// Cholesky decomposition from faer failed.
37 #[error("The faer Cholesky failed: {0}")]
38 FaerCholeskyError(#[from] faer::linalg::solvers::LltError),
39
40 // -- PCA with missing values --
41 /// A row or column of the input to PPCA / BPCA holds no observed value.
42 #[error("PCA with missing values: {axis} {index} is entirely missing; remove it first.")]
43 PcaAllMissing {
44 /// `"row"` or `"column"`
45 axis: &'static str,
46 /// Zero-based index of the offending row or column
47 index: usize,
48 },
49
50 /// More components were requested than the data can hold.
51 #[error("PCA: requested {n_pcs} components, but at most {max} are possible.")]
52 PcaTooManyComponents {
53 /// Requested number of components
54 n_pcs: usize,
55 /// `min(nrow, ncol)`
56 max: usize,
57 },
58
59 // -- Kernel smoothing --
60 /// A kernel bandwidth was requested for fewer than two points.
61 #[error("Kernel bandwidth needs at least 2 data points, got {found}.")]
62 BandwidthTooFewPoints {
63 /// Number of points supplied
64 found: usize,
65 },
66
67 /// A Sheather-Jones pilot functional came back non-finite or non-positive.
68 ///
69 /// R raises "sample is too sparse to find TD" / "... alph2" here. It means
70 /// the pair-distance counts carry too little signal for the pilot estimate,
71 /// which in practice is a degenerate or near-constant sample.
72 #[error("Sheather-Jones bandwidth: sample too sparse to find {stage}.")]
73 BandwidthTooSparse {
74 /// Which pilot functional failed, `TD` or `alph2`
75 stage: &'static str,
76 },
77
78 /// The Sheather-Jones bracket search never enclosed a root.
79 #[error("Sheather-Jones bandwidth: no solution in the search interval.")]
80 BandwidthNoBracket,
81
82 /// Two vectors that had to agree in length did not.
83 #[error("Length mismatch for '{name}': expected {expected}, got {found}.")]
84 LengthMismatch {
85 /// Name of the offending input
86 name: &'static str,
87 /// Length that was required
88 expected: usize,
89 /// Length that was supplied
90 found: usize,
91 },
92
93 // -- Gaussian processes --
94 /// A Gaussian process was handed empty training data.
95 #[error("Gaussian process: {name} is empty")]
96 GpEmptyInput {
97 /// Which input was empty
98 name: &'static str,
99 },
100
101 /// The training inputs and responses disagree in length.
102 #[error(
103 "Gaussian process: {n_x} training inputs against {n_y} response rows; the two must be aligned"
104 )]
105 GpDimensionMismatch {
106 /// Length of the input vector
107 n_x: usize,
108 /// Rows in the response matrix
109 n_y: usize,
110 },
111
112 /// A hyperparameter is outside its supported range.
113 #[error("Gaussian process: hyperparameter '{name}' is {value}, which is not supported")]
114 GpInvalidHyperparameter {
115 /// Name of the offending hyperparameter
116 name: &'static str,
117 /// The value that was rejected
118 value: f64,
119 },
120
121 /// A non-finite value reached the fit.
122 ///
123 /// Checked up front because a `NaN` otherwise surfaces as an unexplained
124 /// Cholesky failure much later, with nothing pointing back at the input.
125 #[error("Gaussian process: {name} contains a non-finite value")]
126 GpNonFiniteInput {
127 /// Which input carried the non-finite value
128 name: &'static str,
129 },
130
131 /// The landmark Gram matrix stayed singular through the whole jitter ladder.
132 #[error(
133 "Gaussian process: the {n_landmarks} x {n_landmarks} landmark Gram is not positive definite even at a jitter of {jitter}; spread the landmarks out or shorten the length scale"
134 )]
135 GpLandmarkCholeskyFailed {
136 /// Largest jitter that was tried
137 jitter: f64,
138 /// Number of landmarks
139 n_landmarks: usize,
140 },
141
142 /// The posterior Cholesky failed.
143 ///
144 /// Distinct from [BixverseErrors::GpLandmarkCholeskyFailed] because this
145 /// factorises `A A^T / sigma^2 + I`, whose smallest eigenvalue is at least
146 /// one in exact arithmetic. A failure here means a non-finite value got
147 /// through the input checks, not that the problem is ill-conditioned.
148 #[error(
149 "Gaussian process: the posterior Cholesky failed, which should be impossible for a matrix with unit diagonal dominance; the accumulation produced non-finite entries"
150 )]
151 GpPosteriorCholeskyFailed,
152
153 /// Every landmark sits at the same coordinate.
154 #[error(
155 "Gaussian process: all landmarks share the same coordinate, so the kernel matrix is degenerate"
156 )]
157 GpDegenerateLandmarks,
158
159 /// Every sample weight is zero.
160 ///
161 /// A zero weight drops its observation, so this leaves the fit with no data
162 /// and returns the prior everywhere. Indistinguishable from a genuinely
163 /// flat response, hence an error rather than a silent zero curve.
164 #[error("Gaussian process: every sample weight is zero, so the fit would carry no data")]
165 GpAllWeightsZero,
166
167 // -- statrs --
168 /// Beta distribution construction failed
169 #[error("Beta distribution error: {0}")]
170 BetaDistribution(String),
171
172 // -- ann-search-rs --
173 /// Propagate errors from the ann-search-rs crate
174 #[error("Error from the ann-search-rs crate: {0}")]
175 AnnSearchRsError(#[from] ann_search_rs::errors::AnnSearchErrors),
176
177 // -- distances --
178 /// Distance type not supported
179 #[error("Distance metric '{0}' is not supported for this method.")]
180 DistanceNotSupported(Dist),
181
182 // -- matrix algebra errors --
183 /// Error if the feature dimensions are not the same
184 #[error("The feature dimensions between the two matrices are unequal! x: {dim_x}, y: {dim_y}")]
185 NonMatchingFeatureDim {
186 /// Feature dimensions matrix x
187 dim_x: usize,
188 /// Feature dimensions matrix y
189 dim_y: usize,
190 },
191
192 /// Label length and number of cells do not match for Harmony
193 #[error(
194 "The labels length ({label_length}) does not match the number of samples ({n_samples})"
195 )]
196 NumberLabelsNotEqualSampleNumber {
197 /// Provided label length
198 label_length: usize,
199 /// Number of cells
200 n_samples: usize,
201 },
202
203 /// If the reference is out of range
204 #[error("The provided reference is out of range")]
205 ReferenceOutOfRange,
206
207 // -- sparse erros --
208 /// Error in situations were data_2 in [`crate::prelude::CompressedSparseData2`]
209 /// is asked for but not available
210 #[error("data_2 slot is None but was requested")]
211 Data2NotAvailable,
212
213 /// Error if the slice index is out of bounds
214 #[error("The slice index ({index}) is out of bounds (length: {len})")]
215 SliceIndexOutOfBounds {
216 /// The chosen index
217 index: usize,
218 /// The actual length
219 len: usize,
220 },
221
222 /// Error if the slice index is duplicated
223 #[error("You provided a duplicated slice index {0}")]
224 DuplicateSliceIndex(usize),
225
226 /// Error for sparse multiplication dimension mismatches
227 #[error(
228 "The dimensions of the matrix do not support sparse multiplication (matrix a n_col: {n_col_a}; matrix b n_row: {n_row_b})"
229 )]
230 SparseMatrixMultiplication {
231 /// Number of columns in matrix a
232 n_col_a: usize,
233 /// Number of rows in matrix b
234 n_row_b: usize,
235 },
236
237 /// Dimension mismatch error during construction
238 #[error("The indices (len: {indices_len}) and data (len: {data_len}) need to have same length")]
239 DimensionMisMatchSparse {
240 /// Length indices
241 indices_len: usize,
242 /// Length data
243 data_len: usize,
244 },
245
246 /// Error if the `indptr` of a [`crate::prelude::CompressedSparseData2`] is
247 /// inconsistent with the shape it declares or with the stored values.
248 ///
249 /// Raised by [`crate::prelude::CompressedSparseData2::validate`] on input
250 /// that crossed an FFI boundary, where the struct's public fields could
251 /// have been populated with anything.
252 #[error("The sparse indptr is inconsistent ({detail}): expected {expected}, got {got}")]
253 SparseIndptrInvalid {
254 /// Which invariant failed
255 detail: &'static str,
256 /// The value implied by the rest of the structure
257 expected: usize,
258 /// The value actually found
259 got: usize,
260 },
261
262 /// Error if there is a dimension mismatch in terms of the two matrices
263 #[error("The shape of the two sparse matrices must be the same.")]
264 ShapeMismatchSparse,
265
266 /// Error if the [crate::prelude::CompressedSparseData2] is not in CSR.
267 #[error("The SparseCompressedData2 must be in CSR format")]
268 SparseMatrixMustBeCsr,
269
270 /// A row summed to (effectively) zero where a row-stochastic normalisation
271 /// was asked for.
272 ///
273 /// On a kNN-derived affinity matrix every node has `k` out-edges, so a zero
274 /// row means the kernel weights underflowed rather than that the graph is
275 /// legitimately disconnected. Anything built on top of the operator would
276 /// be meaningless, hence an error rather than leaving the row at zero.
277 #[error(
278 "Row {row} of the sparse matrix sums to {row_sum}, which is not positive, so it cannot be normalised to a row-stochastic form (isolated node, or the kernel weights underflowed)"
279 )]
280 SparseMatrixIsolatedRow {
281 /// The offending row index
282 row: usize,
283 /// The row sum that was rejected
284 row_sum: f64,
285 },
286
287 /// A CSR matrix failed one of its structural invariants.
288 ///
289 /// Raised by [crate::core::math::sparse::validate_square_csr]. The static
290 /// string names the invariant, because "the graph is malformed" alone leaves
291 /// the caller to bisect their own input.
292 #[error("The CSR matrix is malformed: {0}")]
293 MalformedCsr(&'static str),
294
295 /// Error if the [crate::prelude::CompressedSparseData2] is not in Csc.
296 #[error("The SparseCompressedData2 must be in CSC format")]
297 SparseMatrixMustBeCsc,
298
299 /// Shape mismatch problem for matrices
300 #[error("Expected this shape {expected:?}; got {got:?}.")]
301 ShapeMismatch {
302 /// Expected shape
303 expected: (usize, usize),
304 /// Provided shape
305 got: (usize, usize),
306 },
307
308 /// General error for sparse matrix format mismatches
309 #[error("Expected this compressed sparse format {expected}; got {got}.")]
310 SparseLayoutMismatch {
311 /// The expected [CompressedSparseFormat] enum.
312 expected: CompressedSparseFormat,
313 /// The provided [CompressedSparseFormat] enum.
314 got: CompressedSparseFormat,
315 },
316}