Skip to main content

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 {}