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