Skip to main content

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}