Skip to main content

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
255/// Sparse structure errors surface through the core variant, as before the
256/// sparse structures moved to `bixverse-sparse`.
257impl From<bixverse_core::errors::SparseErrors> for MethodsErrors {
258    fn from(err: bixverse_core::errors::SparseErrors) -> Self {
259        MethodsErrors::Core(err.into())
260    }
261}