Skip to main content

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}