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