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        }
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}