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