edge_rs/errors.rs
1//! The crate-level error enum.
2//!
3//! One `thiserror` enum for the whole crate, grouped by subsystem. Add new
4//! variants to the matching section, not the bottom.
5//!
6//! Anything a caller could plausibly hit returns `Err`. Panics are for broken
7//! invariants only.
8
9use thiserror::Error;
10
11/// Every failure mode in the crate.
12#[derive(Debug, Error)]
13pub enum EdgeErrors {
14 // --------- //
15 // arguments //
16 // --------- //
17 /// A parameter fell outside its documented domain.
18 #[error("Invalid argument: {0}")]
19 InvalidArgument(String),
20
21 /// A count, size or index that must be non-zero was zero.
22 #[error("Parameter '{0}' must be a positive non-zero integer.")]
23 MustBePositive(String),
24
25 /// A named method string did not match any known variant.
26 #[error("Unknown {kind} method '{name}'. Expected one of: {expected}.")]
27 UnknownMethod {
28 /// What kind of method was being selected, e.g. "normalisation"
29 kind: &'static str,
30 /// The string the caller supplied
31 name: String,
32 /// Comma-separated list of accepted values
33 expected: &'static str,
34 },
35
36 // ---------- //
37 // dimensions //
38 // ---------- //
39 /// Two operands disagreed on shape.
40 #[error("Expected shape {expected:?}; got {got:?}.")]
41 ShapeMismatch {
42 /// Shape that was required
43 expected: (usize, usize),
44 /// Shape that was supplied
45 got: (usize, usize),
46 },
47
48 /// A vector was not the length its consumer required.
49 #[error("'{name}' has length {got}, expected {expected}.")]
50 LengthMismatch {
51 /// Name of the offending argument
52 name: &'static str,
53 /// Length that was required
54 expected: usize,
55 /// Length that was supplied
56 got: usize,
57 },
58
59 /// The counts matrix had no genes or no samples.
60 #[error("Counts matrix is empty: {n_genes} genes by {n_samples} samples.")]
61 EmptyCounts {
62 /// Number of genes (rows)
63 n_genes: usize,
64 /// Number of samples (columns)
65 n_samples: usize,
66 },
67
68 // --------------- //
69 // design matrices //
70 // --------------- //
71 /// The design matrix is rank deficient, so coefficients are not identifiable.
72 #[error("Design matrix is not full rank: {n_cols} columns but rank {rank}.")]
73 DesignNotFullRank {
74 /// Number of columns in the design
75 n_cols: usize,
76 /// Numerical rank actually found
77 rank: usize,
78 },
79
80 /// A contrast put weight on a coefficient the design cannot estimate.
81 #[error(
82 "Contrast {contrast} puts weight on coefficient {coef}, which is aliased and so has no estimate."
83 )]
84 NotEstimableContrast {
85 /// Index of the offending contrast
86 contrast: usize,
87 /// Index of the non-estimable coefficient it touches
88 coef: usize,
89 },
90
91 /// A contrast or coefficient index pointed outside the design.
92 #[error("Coefficient index {index} is out of range for a design with {n_coef} coefficients.")]
93 CoefOutOfRange {
94 /// Index that was requested
95 index: usize,
96 /// Number of coefficients available
97 n_coef: usize,
98 },
99
100 // -------------- //
101 // linear algebra //
102 // -------------- //
103 /// A Cholesky factorisation failed, meaning the matrix was not positive definite.
104 #[error("Cholesky factorisation failed: {0}")]
105 CholeskyFailed(String),
106
107 /// A linear solve produced a non-finite result.
108 #[error("Linear solve failed: {0}")]
109 SolveFailed(String),
110
111 /// A symmetric eigendecomposition did not converge.
112 #[error("Eigendecomposition failed: {0}")]
113 EigenFailed(String),
114
115 // --------------- //
116 // numeric support //
117 // --------------- //
118 /// A scalar optimiser or root finder did not converge inside its iteration budget.
119 #[error(
120 "{routine} failed to converge after {iterations} iterations (last change {last_delta:e})."
121 )]
122 NoConvergence {
123 /// Name of the routine that gave up
124 routine: &'static str,
125 /// Iterations actually performed
126 iterations: usize,
127 /// Magnitude of the final update
128 last_delta: f64,
129 },
130
131 /// A bracketing root finder was handed an interval that does not bracket a root.
132 #[error(
133 "Interval [{lower}, {upper}] does not bracket a root: f(lower) = {f_lower:e}, f(upper) = {f_upper:e}."
134 )]
135 RootNotBracketed {
136 /// Lower end of the interval
137 lower: f64,
138 /// Upper end of the interval
139 upper: f64,
140 /// Function value at `lower`
141 f_lower: f64,
142 /// Function value at `upper`
143 f_upper: f64,
144 },
145
146 /// A bounded optimisation was given a box with a lower bound above its upper bound.
147 #[error("Invalid bounds for parameter {index}: lower {lower} exceeds upper {upper}.")]
148 InvalidBounds {
149 /// Index of the offending parameter
150 index: usize,
151 /// Lower bound supplied
152 lower: f64,
153 /// Upper bound supplied
154 upper: f64,
155 },
156
157 // ------ //
158 // sparse //
159 // ------ //
160 /// A compressed sparse structure violated its own invariants.
161 #[error("Malformed sparse matrix: {0}")]
162 MalformedSparse(String),
163
164 // ------------- //
165 // normalisation //
166 // ------------- //
167 /// Every gene was filtered out before a normalisation factor could be computed.
168 #[error("No genes survived filtering for normalisation; all {n_genes} were dropped.")]
169 NoGenesAfterFiltering {
170 /// Number of genes present before filtering
171 n_genes: usize,
172 },
173
174 /// The reference column for TMM could not be chosen or was out of range.
175 #[error("Invalid TMM reference column: {0}")]
176 InvalidReferenceColumn(String),
177
178 // --- //
179 // GLM //
180 // --- //
181 /// A dispersion outside `[0, inf)` was supplied to a negative binomial routine.
182 #[error("Dispersion must be non-negative and finite; got {0}.")]
183 InvalidDispersion(f64),
184
185 /// An adjusted profile likelihood was asked for before a gene was loaded.
186 #[error("No gene has been loaded into the workspace; call `begin_gene` first.")]
187 AplWorkspaceNotStarted,
188
189 // -------------------- //
190 // single cell / NEBULA //
191 // -------------------- //
192 /// Cells were not grouped contiguously by subject, which every inner loop assumes.
193 #[error(
194 "Cells are not contiguous by subject: subject {subject} appears in more than one block."
195 )]
196 SubjectsNotContiguous {
197 /// Identifier of the subject found in multiple blocks
198 subject: usize,
199 },
200
201 /// The design matrix handed to NEBULA has no intercept column.
202 #[error("NEBULA requires an intercept column in the design matrix; none was found.")]
203 MissingIntercept,
204
205 /// Fewer subjects than the mixed model can identify a random effect from.
206 #[error("NEBULA needs at least {required} subjects; got {got}.")]
207 TooFewSubjects {
208 /// Minimum number of subjects needed
209 required: usize,
210 /// Number of subjects supplied
211 got: usize,
212 },
213
214 // --- //
215 // GPU //
216 // --- //
217 /// The CubeCL runtime or a device limit rejected the work.
218 ///
219 /// A busted device limit does not fail loudly on wgpu, so this is raised
220 /// from the validated dispatch geometry, not from the launch.
221 #[cfg(feature = "gpu")]
222 #[error("GPU error: {0}")]
223 Gpu(String),
224}