bixverse_methods/errors.rs
1//! Errors raised by `bixverse-methods`.
2
3use thiserror::Error;
4
5/// Error variants raised by `bixverse-methods`.
6///
7/// Grouped by subsystem.
8#[derive(Debug, Error)]
9pub enum MethodsErrors {
10 // -- external crates --
11 /// Propagates an error from `ann-search-rs`.
12 #[error("Error from the ann-search-rs crate: {0}")]
13 AnnSearchRsError(#[from] ann_search_rs::errors::AnnSearchErrors),
14
15 // -- lower crates --
16 /// Propagates an error from `bixverse-core`.
17 #[error(transparent)]
18 Core(#[from] bixverse_core::errors::CoreErrors),
19
20 // -- edge-rs --
21 /// Propagate errors from the edge-rs crate
22 #[cfg(feature = "dge")]
23 #[error("Error from the edge-rs crate: {0}")]
24 EdgeRsError(#[from] edge_rs::errors::EdgeErrors),
25
26 /// More than one coefficient was handed to the limma route.
27 ///
28 /// `topTable` tabulates a single column. Testing several coefficients at
29 /// once is the moderated F question, which needs `top_table_f` rather than
30 /// this chain.
31 #[cfg(feature = "dge")]
32 #[error(
33 "limma tests a single coefficient or contrast at a time; {n_coef} coefficients were given"
34 )]
35 LimmaMultiCoef {
36 /// Number of coefficients the caller asked to test together
37 n_coef: usize,
38 },
39
40 /// An input vector does not line up with the observations being modelled.
41 ///
42 /// Covers the design rows, the subject labels and the offsets, all of which
43 /// have to be as long as the cell or sample selection.
44 #[cfg(feature = "dge")]
45 #[error("Differential expression: {name} has {got} entries but {expected} were expected")]
46 DgeShapeMismatch {
47 /// Which input is the wrong length
48 name: &'static str,
49 /// What the selection implies
50 expected: usize,
51 /// What was handed over
52 got: usize,
53 },
54
55 // -- graph based errors ---
56 /// Error for algorithms that expect undirected graphs
57 ///
58 /// For methods that expect an undirected graph, but received a directed
59 /// one.
60 #[error("The Graph is directed but needs to be undirected for this algorithm.")]
61 GraphDirectedError,
62
63 /// A node index in an edge list points outside the graph
64 #[error("Graph: {name} index {index} is out of range for {n_nodes} nodes.")]
65 GraphIndexOutOfRange {
66 /// Which input carried the index
67 name: &'static str,
68 /// The offending index
69 index: usize,
70 /// Number of nodes in the graph
71 n_nodes: usize,
72 },
73
74 /// A per-node or per-edge input does not line up with the graph
75 #[error("Graph: {name} has length {got}, but {expected} was expected.")]
76 GraphLengthMismatch {
77 /// Which input is the wrong length
78 name: &'static str,
79 /// Length the graph implies
80 expected: usize,
81 /// Length that was supplied
82 got: usize,
83 },
84
85 /// The UMAP kNN graph was asked for without kNN distances
86 #[error("Graph: the UMAP kNN symmetrisation needs the kNN distances.")]
87 KnnGraphNeedsDistances,
88
89 /// A node type present in the graph has no type weight
90 #[error("Graph: node type '{0}' has no type weight.")]
91 MissingNodeTypeWeight(String),
92
93 /// Error for community membership/graph node number mismatch
94 ///
95 /// In cases where the graph and the community membership do not agree.
96 #[error("The number of nodes and membership assignments in the communities do not add up.")]
97 CommunityAssignmentMismatch,
98
99 /// Error if a partition label sits outside the declared partition count
100 #[error("The partition label {label} is out of range for {n_partitions} partitions.")]
101 PartitionLabelOutOfRange {
102 /// The offending label
103 label: usize,
104 /// Number of declared partitions
105 n_partitions: usize,
106 },
107
108 /// Error if the partition count exceeds what the dense coarsening accepts
109 #[error(
110 "The number of partitions ({n_partitions}) exceeds the maximum of {max} for graph abstraction."
111 )]
112 TooManyPartitions {
113 /// Provided number of partitions
114 n_partitions: usize,
115 /// The supported maximum
116 max: usize,
117 },
118
119 // -- NMF --
120 /// NMF Rank is too large for the NNDSVD initialisation
121 #[error(
122 "The requested NMF ({requested}) rank is too large for NNDSVD initialisation (available: {available})."
123 )]
124 NmfRankTooLarge {
125 /// Requested rank
126 requested: usize,
127 /// Maximum available rank
128 available: usize,
129 },
130
131 /// Error if NMF values are not finite
132 #[error("The NMF values are not finite")]
133 NmfNonFinite,
134
135 /// Error if NMF values are negative
136 #[error("Negative values were discovered for NMF. Please check the inputs.")]
137 NmfNonNegativeViolated,
138
139 /// Consensus NMF received a rank that cannot support a silhouette.
140 #[error("Consensus NMF needs k >= 2, but k = {k} was requested.")]
141 NmfConsensusInvalidK {
142 /// Requested rank
143 k: usize,
144 },
145
146 /// Consensus NMF received too few restarts to pool over.
147 #[error("Consensus NMF needs at least 2 restarts, but n_runs = {n_runs} was requested.")]
148 NmfConsensusTooFewRuns {
149 /// Requested number of restarts
150 n_runs: usize,
151 },
152
153 /// The outlier filter removed so many components that k-means cannot run.
154 #[error(
155 "Consensus NMF: only {n_surviving} components survived the density filter, which is fewer than k = {k}. Raise the density threshold or add restarts."
156 )]
157 NmfConsensusTooFewComponents {
158 /// Requested rank
159 k: usize,
160 /// Components left after filtering
161 n_surviving: usize,
162 },
163
164 /// k-means returned an empty cluster, so the consensus factor would carry a
165 /// zero row and the refit would be degenerate.
166 #[error(
167 "Consensus NMF: cluster {cluster} of {k} is empty, so the consensus factor is degenerate."
168 )]
169 NmfConsensusEmptyCluster {
170 /// Index of the empty cluster
171 cluster: usize,
172 /// Requested rank
173 k: usize,
174 },
175
176 /// The k sweep was handed nothing to sweep over.
177 #[error("The NMF k sweep was given an empty k range.")]
178 NmfKSweepEmptyRange,
179
180 /// A consensus step was applied to restarts run at a different rank.
181 #[error(
182 "Consensus NMF was asked for k = {requested}, but the restarts were run at k = {restarts}."
183 )]
184 NmfConsensusKMismatch {
185 /// Rank requested for the consensus
186 requested: usize,
187 /// Rank the restarts actually hold
188 restarts: usize,
189 },
190
191 /// A frozen factor handed to a refit does not line up with V.
192 #[error(
193 "NMF refit: the frozen factor has a shared dimension of {found}, but V expects {expected}."
194 )]
195 NmfDimensionMismatch {
196 /// Dimension V expects
197 expected: usize,
198 /// Dimension the frozen factor provides
199 found: usize,
200 },
201
202 // -- LDA --
203 /// The requested topic count cannot be supported by the corpus.
204 ///
205 /// A topic needs somewhere to put its mass, so `k` above the vocabulary
206 /// size leaves topics that no document can ever distinguish.
207 #[error(
208 "LDA: the requested topic count ({requested}) exceeds what the corpus supports (max: {max_available})."
209 )]
210 LdaInvalidTopicCount {
211 /// Requested number of topics
212 requested: usize,
213 /// Largest topic count the corpus supports
214 max_available: usize,
215 },
216
217 /// A Dirichlet hyperparameter is not strictly positive.
218 #[error("LDA: hyperparameter '{name}' must be strictly positive, got {value}.")]
219 LdaInvalidHyperparameter {
220 /// Name of the offending hyperparameter
221 name: String,
222 /// Value that was supplied
223 value: f64,
224 },
225
226 /// The document-term matrix carries no counts at all.
227 #[error("LDA: the document-term matrix holds no non-zero entries.")]
228 LdaEmptyMatrix,
229
230 /// A non-finite value reached the variational parameters.
231 #[error("LDA: the variational parameters became non-finite. Please check the inputs.")]
232 LdaNonFinite,
233
234 /// Coherence was asked for more top terms than the vocabulary holds.
235 #[error(
236 "LDA: coherence requested the top {requested} terms, but the vocabulary holds {vocab_size}."
237 )]
238 LdaTopNTooLarge {
239 /// Requested number of top terms per topic
240 requested: usize,
241 /// Vocabulary size available
242 vocab_size: usize,
243 },
244
245 /// Metric inputs disagree on the number of documents or topics.
246 #[error("LDA metrics: expected a dimension of {expected}, but received {got}.")]
247 LdaDimensionMismatch {
248 /// Dimension the model implies
249 expected: usize,
250 /// Dimension that was supplied
251 got: usize,
252 },
253
254 // -- linear model activity --
255 /// A feature set points at a feature outside the feature universe.
256 #[error("Feature set {set}: feature index {index} is out of range for {n_features} features.")]
257 FeatureSetIndexOutOfRange {
258 /// Index of the offending set
259 set: usize,
260 /// The offending feature index
261 index: usize,
262 /// Size of the feature universe
263 n_features: usize,
264 },
265
266 /// A feature set is empty, repeats a feature, carries a non-finite weight
267 /// or has a weight vector of the wrong length.
268 #[error("Feature set {set} is invalid: {reason}.")]
269 FeatureSetInvalid {
270 /// Index of the offending set
271 set: usize,
272 /// What is wrong with it
273 reason: &'static str,
274 },
275
276 /// The data and the feature sets disagree on the number of features.
277 #[error(
278 "Linear model activity: the data has {got} features, the feature sets expect {expected}."
279 )]
280 LmActivityShapeMismatch {
281 /// Feature universe of the feature sets
282 expected: usize,
283 /// Features in the data
284 got: usize,
285 },
286
287 /// The model leaves no residual degrees of freedom.
288 #[error(
289 "Linear model activity: {n_features} features cannot fit {n_params} parameters with residual degrees of freedom left."
290 )]
291 LmActivityDegreesOfFreedom {
292 /// Number of features (the observations of each fit)
293 n_features: usize,
294 /// Number of fitted parameters, intercept included
295 n_params: usize,
296 },
297
298 /// The MLM design is (numerically) rank deficient.
299 ///
300 /// Two sets with identical or proportional weights, or a set covering every
301 /// feature with one weight (collinear with the intercept), make `XᵀX`
302 /// singular.
303 #[error(
304 "MLM: the feature set design is rank deficient (eigenvalue ratio {ratio:e}); drop or merge collinear sets."
305 )]
306 LmActivityRankDeficient {
307 /// Smallest over largest eigenvalue of `XᵀX` scaled to unit diagonal
308 ratio: f64,
309 },
310
311 /// An observation holds a NaN or infinite value.
312 #[error(
313 "Linear model activity: observation {observation} of the batch contains NaN or infinite values."
314 )]
315 LmActivityNonFinite {
316 /// Index of the observation within the scored batch
317 observation: usize,
318 },
319}
320
321/// Sparse structure errors surface through the core variant, as before the
322/// sparse structures moved to `bixverse-sparse`.
323impl From<bixverse_core::errors::SparseErrors> for MethodsErrors {
324 fn from(err: bixverse_core::errors::SparseErrors) -> Self {
325 MethodsErrors::Core(err.into())
326 }
327}