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