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
//! The crate-level error enum.
//!
//! One `thiserror` enum for all of `edge-rs`. Variants are grouped by subsystem
//! with section comments; add new ones to the matching section rather than the
//! bottom, so the file stays navigable as the port grows.
//!
//! Anything a caller could plausibly hit returns `Err` rather than panicking.
//! Panics are reserved for broken invariants that the type system cannot state.
use thiserror::Error;
/// Every failure mode in the crate.
#[derive(Debug, Error)]
pub enum EdgeErrors {
// -- arguments --
/// A parameter fell outside its documented domain.
#[error("Invalid argument: {0}")]
InvalidArgument(String),
/// A count, size or index that must be non-zero was zero.
#[error("Parameter '{0}' must be a positive non-zero integer.")]
MustBePositive(String),
/// A named method string did not match any known variant.
#[error("Unknown {kind} method '{name}'. Expected one of: {expected}.")]
UnknownMethod {
/// What kind of method was being selected, e.g. "normalisation"
kind: &'static str,
/// The string the caller supplied
name: String,
/// Comma-separated list of accepted values
expected: &'static str,
},
// -- dimensions --
/// Two operands disagreed on shape.
#[error("Expected shape {expected:?}; got {got:?}.")]
ShapeMismatch {
/// Shape that was required
expected: (usize, usize),
/// Shape that was supplied
got: (usize, usize),
},
/// A vector was not the length its consumer required.
#[error("'{name}' has length {got}, expected {expected}.")]
LengthMismatch {
/// Name of the offending argument
name: &'static str,
/// Length that was required
expected: usize,
/// Length that was supplied
got: usize,
},
/// The counts matrix had no genes or no samples.
///
/// Every downstream estimator divides by one of these, so an empty input is
/// an error rather than a trivially empty result.
#[error("Counts matrix is empty: {n_genes} genes by {n_samples} samples.")]
EmptyCounts {
/// Number of genes (rows)
n_genes: usize,
/// Number of samples (columns)
n_samples: usize,
},
// -- design matrices --
/// The design matrix is rank deficient, so coefficients are not identifiable.
#[error("Design matrix is not full rank: {n_cols} columns but rank {rank}.")]
DesignNotFullRank {
/// Number of columns in the design
n_cols: usize,
/// Numerical rank actually found
rank: usize,
},
/// A contrast put weight on a coefficient the design cannot estimate.
#[error(
"Contrast {contrast} puts weight on coefficient {coef}, which is aliased and so has no estimate."
)]
NotEstimableContrast {
/// Index of the offending contrast
contrast: usize,
/// Index of the non-estimable coefficient it touches
coef: usize,
},
/// A contrast or coefficient index pointed outside the design.
#[error("Coefficient index {index} is out of range for a design with {n_coef} coefficients.")]
CoefOutOfRange {
/// Index that was requested
index: usize,
/// Number of coefficients available
n_coef: usize,
},
// -- linear algebra --
/// A Cholesky factorisation failed, meaning the matrix was not positive definite.
#[error("Cholesky factorisation failed: {0}")]
CholeskyFailed(String),
/// A linear solve produced a non-finite result.
#[error("Linear solve failed: {0}")]
SolveFailed(String),
/// A symmetric eigendecomposition did not converge.
#[error("Eigendecomposition failed: {0}")]
EigenFailed(String),
// -- numeric support --
/// A scalar optimiser or root finder did not converge inside its iteration budget.
#[error(
"{routine} failed to converge after {iterations} iterations (last change {last_delta:e})."
)]
NoConvergence {
/// Name of the routine that gave up
routine: &'static str,
/// Iterations actually performed
iterations: usize,
/// Magnitude of the final update
last_delta: f64,
},
/// A bracketing root finder was handed an interval that does not bracket a root.
#[error(
"Interval [{lower}, {upper}] does not bracket a root: f(lower) = {f_lower:e}, f(upper) = {f_upper:e}."
)]
RootNotBracketed {
/// Lower end of the interval
lower: f64,
/// Upper end of the interval
upper: f64,
/// Function value at `lower`
f_lower: f64,
/// Function value at `upper`
f_upper: f64,
},
/// A bounded optimisation was given a box with a lower bound above its upper bound.
#[error("Invalid bounds for parameter {index}: lower {lower} exceeds upper {upper}.")]
InvalidBounds {
/// Index of the offending parameter
index: usize,
/// Lower bound supplied
lower: f64,
/// Upper bound supplied
upper: f64,
},
// -- sparse --
/// A compressed sparse structure violated its own invariants.
#[error("Malformed sparse matrix: {0}")]
MalformedSparse(String),
// -- normalisation --
/// Every gene was filtered out before a normalisation factor could be computed.
#[error("No genes survived filtering for normalisation; all {n_genes} were dropped.")]
NoGenesAfterFiltering {
/// Number of genes present before filtering
n_genes: usize,
},
/// The reference column for TMM could not be chosen or was out of range.
#[error("Invalid TMM reference column: {0}")]
InvalidReferenceColumn(String),
// -- GLM --
/// A dispersion outside `[0, inf)` was supplied to a negative binomial routine.
#[error("Dispersion must be non-negative and finite; got {0}.")]
InvalidDispersion(f64),
/// An adjusted profile likelihood was asked for before a gene was loaded.
#[error("No gene has been loaded into the workspace; call `begin_gene` first.")]
AplWorkspaceNotStarted,
// -- single cell / NEBULA --
/// Cells were not grouped contiguously by subject, which every inner loop assumes.
#[error(
"Cells are not contiguous by subject: subject {subject} appears in more than one block."
)]
SubjectsNotContiguous {
/// Identifier of the subject found in multiple blocks
subject: usize,
},
/// The design matrix handed to NEBULA has no intercept column.
///
/// Centring and the starting values both key off the intercept, so its
/// absence is a hard error rather than something to work around.
#[error("NEBULA requires an intercept column in the design matrix; none was found.")]
MissingIntercept,
/// Fewer subjects than the mixed model can identify a random effect from.
#[error("NEBULA needs at least {required} subjects; got {got}.")]
TooFewSubjects {
/// Minimum number of subjects needed
required: usize,
/// Number of subjects supplied
got: usize,
},
// -- GPU --
/// The CubeCL runtime or a device limit rejected the work.
///
/// Carries the runtime's own message. A busted device limit does not fail
/// loudly on wgpu, so this is raised from the validated dispatch geometry
/// rather than from the launch itself.
#[cfg(feature = "gpu")]
#[error("GPU error: {0}")]
Gpu(String),
}