Skip to main content

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}