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