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