arrowspace/error.rs
1//! Error types for fallible ArrowSpace operations.
2//!
3//! These errors represent **recoverable input conditions** — not invariant
4//! violations — and are returned by the `try_*` family of methods so that FFI
5//! bindings can surface typed exceptions instead of relying on Rust panics
6//! (which cross language boundaries as opaque `PanicException`).
7//!
8//! The infallible wrappers (`prepare_query_item`, `search_lambda_aware`, …)
9//! delegate to the `try_*` variants and `.expect()` on `Err`, preserving their
10//! original panic behaviour for existing callers. Since #153 they are
11//! `#[deprecated]`: the `try_*` variants are the primary documented entry
12//! points, and the wrappers remain only as a backward-compatibility surface
13//! for downstream callers that have not migrated yet.
14
15use std::fmt;
16
17/// Recoverable error conditions arising from the query path.
18///
19/// # When each variant occurs
20///
21/// | Variant | Trigger |
22/// |---------|---------|
23/// | `DegenerateLambda` | A query or item has a spectral score (λ) of ~0, making λ-aware ranking undecidable. Usually a sign of a mis-tuned `eps` or an out-of-context query. |
24/// | `NonFiniteQuery` | The query vector contains `NaN` or `±Infinity`. |
25/// | `DimensionMismatch` | The query vector's length differs from the index's feature count. |
26/// | `EmptyItems` | The item matrix passed to the builder is empty. |
27/// | `InvalidConfig` | The builder configuration is invalid for the requested pipeline (e.g. energy build without dims reduction). |
28/// | `EnergyModeRequired` | Energy motif/subgraph spotting lacks energy-mode bookkeeping. |
29/// | `EigenModeRequired` | Item-space eigen motif spotting lacks EigenMaps bookkeeping. |
30#[derive(Clone, Debug, PartialEq)]
31pub enum ArrowSpaceError {
32 /// The spectral score (lambda) computed for a query — or stored on a query
33 /// item — is approximately zero, so lambda-aware similarity cannot produce
34 /// a meaningful ranking.
35 ///
36 /// `raw` is the offending lambda value so callers can decide whether it was
37 /// a stored zero (caller forgot to call `prepare_query_item`) or a computed
38 /// zero (the graph Laplacian maps the query to its null space).
39 DegenerateLambda {
40 /// The degenerate lambda value that triggered the error.
41 raw: f64,
42 },
43
44 /// The query vector contains one or more non-finite values (`NaN`,
45 /// `+Infinity`, `-Infinity`).
46 NonFiniteQuery,
47
48 /// The query vector's dimensionality does not match the index's feature
49 /// count.
50 DimensionMismatch {
51 /// Expected dimension (the index's `nfeatures`).
52 expected: usize,
53 /// Actual dimension of the query vector.
54 got: usize,
55 },
56
57 /// The item matrix passed to the builder is empty.
58 EmptyItems,
59
60 /// The builder configuration is invalid for the requested pipeline.
61 ///
62 /// Triggered by `try_build_energy` when the builder lacks dims reduction
63 /// or carries the (experimental, unimplemented) spectral flag. Replaces
64 /// the `assert!`/`panic!` the energy build path used before (#155) so
65 /// misconfiguration surfaces as a typed error instead of a process abort.
66 InvalidConfig {
67 /// What is wrong with the configuration, as a human-readable fragment.
68 reason: &'static str,
69 },
70
71 /// The operation requires an EnergyMaps build, but the target lacks
72 /// energy-mode bookkeeping.
73 ///
74 /// Triggered by `try_spot_motives_energy` / `try_spot_subg_motives` when
75 /// the Laplacian was not built via `build_energy`, or the index carries no
76 /// `sub_centroids` / `centroid_map`. Running energy motif detection on an
77 /// EigenMaps build would silently operate on feature-space nodes and
78 /// mislabel them as item indices (issue #161), so the operation refuses
79 /// instead of degrading.
80 EnergyModeRequired {
81 /// What is missing, as a human-readable fragment (e.g. `"energy build
82 /// (use build_energy)"`, `"sub_centroids"`, `"centroid_map"`).
83 missing: &'static str,
84 },
85
86 /// The operation requires an EigenMaps build with item→cluster
87 /// bookkeeping, but the target lacks it.
88 ///
89 /// Triggered by `try_spot_motives_eigen` (issue #165) when the Laplacian
90 /// is an EnergyMaps bootstrap map (the energy track has its own
91 /// `try_spot_motives_energy`), or when the index cannot project
92 /// centroid-space motifs onto items: `cluster_assignments` missing or
93 /// not covering every item with an in-range cluster id, `n_clusters < 2`,
94 /// or `aspace.data` absent. Without that projection the only indices
95 /// available are the feature-space nodes of the F×F bootstrap Laplacian,
96 /// and serving them as item indices is the #161 failure family — so the
97 /// operation refuses instead of degrading.
98 EigenModeRequired {
99 /// What is missing, as a human-readable fragment (e.g. `"eigen build
100 /// (use ArrowSpaceBuilder::build)"`, `"a cluster assignment for
101 /// every item"`, `"cluster_assignments values within 0..n_clusters"`).
102 missing: &'static str,
103 },
104}
105
106impl fmt::Display for ArrowSpaceError {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 match self {
109 Self::DegenerateLambda { raw } => write!(
110 f,
111 "degenerate lambda (raw={raw:.6}): lambda is ~0.0, \
112 check the eps parameter for the builder — every dataset has an \
113 optimal eps. The query item may also be out of context for the \
114 dataset (undecidable)."
115 ),
116 Self::NonFiniteQuery => {
117 write!(f, "query item contains non-finite values (NaN or Infinity)")
118 }
119 Self::DimensionMismatch { expected, got } => write!(
120 f,
121 "dimension mismatch: expected {expected} features, got {got}"
122 ),
123 Self::EmptyItems => write!(f, "items cannot be empty"),
124 Self::InvalidConfig { reason } => {
125 write!(f, "invalid configuration: {reason}")
126 }
127 Self::EnergyModeRequired { missing } => write!(
128 f,
129 "energy mode required: missing {missing}. Build via \
130 EnergyMapsBuilder::build_energy so sub_centroids and \
131 centroid_map are populated; running energy motif detection \
132 on an EigenMaps build would mislabel feature-space indices \
133 as item indices."
134 ),
135 Self::EigenModeRequired { missing } => write!(
136 f,
137 "eigen mode required: missing {missing}. Build via \
138 ArrowSpaceBuilder::build so cluster_assignments cover every \
139 item with an in-range cluster id and n_clusters >= 2; \
140 running item-space motif detection without the item→cluster \
141 bookkeeping would mislabel feature-space indices as item \
142 indices (issue #165). For EnergyMaps builds use \
143 try_spot_motives_energy instead."
144 ),
145 }
146 }
147}
148
149impl std::error::Error for ArrowSpaceError {}