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
//! 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,
},
}
/// 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())
}
}