Skip to main content

ruvector_sparsifier_wasm/
lib.rs

1//! WASM bindings for `ruvector-sparsifier`.
2//!
3//! Provides [`WasmSparsifier`] for dynamic spectral graph sparsification
4//! in the browser or any WASM runtime.
5
6use wasm_bindgen::prelude::*;
7
8use ruvector_sparsifier::{
9    AdaptiveGeoSpar, SparseGraph, SparsifierConfig,
10    traits::Sparsifier,
11};
12
13// ---------------------------------------------------------------------------
14// Initialisation
15// ---------------------------------------------------------------------------
16
17/// Initialise the WASM module (sets up the panic hook for better error messages).
18#[wasm_bindgen]
19pub fn init() {
20    console_error_panic_hook::set_once();
21}
22
23/// Return the crate version.
24#[wasm_bindgen]
25pub fn version() -> String {
26    ruvector_sparsifier::VERSION.to_string()
27}
28
29/// Return the default configuration as a JSON string.
30#[wasm_bindgen]
31pub fn default_config() -> String {
32    serde_json::to_string_pretty(&SparsifierConfig::default()).unwrap_or_default()
33}
34
35// ---------------------------------------------------------------------------
36// WasmSparseGraph
37// ---------------------------------------------------------------------------
38
39/// A lightweight sparse graph for building input data.
40#[wasm_bindgen]
41pub struct WasmSparseGraph {
42    inner: SparseGraph,
43}
44
45#[wasm_bindgen]
46impl WasmSparseGraph {
47    /// Create a new empty graph with `n` vertices.
48    #[wasm_bindgen(constructor)]
49    pub fn new(n: u32) -> Self {
50        Self {
51            inner: SparseGraph::with_capacity(n as usize),
52        }
53    }
54
55    /// Add an undirected edge.
56    #[wasm_bindgen(js_name = addEdge)]
57    pub fn add_edge(&mut self, u: u32, v: u32, weight: f64) -> Result<(), JsValue> {
58        self.inner
59            .insert_edge(u as usize, v as usize, weight)
60            .map_err(|e| JsValue::from_str(&e.to_string()))
61    }
62
63    /// Remove an edge.
64    #[wasm_bindgen(js_name = removeEdge)]
65    pub fn remove_edge(&mut self, u: u32, v: u32) -> Result<(), JsValue> {
66        self.inner
67            .delete_edge(u as usize, v as usize)
68            .map(|_| ())
69            .map_err(|e| JsValue::from_str(&e.to_string()))
70    }
71
72    /// Degree of vertex `u`.
73    pub fn degree(&self, u: u32) -> u32 {
74        self.inner.degree(u as usize) as u32
75    }
76
77    /// Number of undirected edges.
78    #[wasm_bindgen(js_name = numEdges)]
79    pub fn num_edges(&self) -> u32 {
80        self.inner.num_edges() as u32
81    }
82
83    /// Number of vertices.
84    #[wasm_bindgen(js_name = numVertices)]
85    pub fn num_vertices(&self) -> u32 {
86        self.inner.num_vertices() as u32
87    }
88
89    /// Serialise the edge list to JSON.
90    #[wasm_bindgen(js_name = toJson)]
91    pub fn to_json(&self) -> String {
92        let edges: Vec<(usize, usize, f64)> = self.inner.edges().collect();
93        serde_json::to_string(&edges).unwrap_or_default()
94    }
95}
96
97// ---------------------------------------------------------------------------
98// WasmSparsifier
99// ---------------------------------------------------------------------------
100
101/// Dynamic spectral graph sparsifier for WASM.
102///
103/// Maintains a compressed shadow graph that preserves the Laplacian energy
104/// of the full graph within `(1 ± epsilon)`.
105#[wasm_bindgen]
106pub struct WasmSparsifier {
107    inner: AdaptiveGeoSpar,
108}
109
110#[wasm_bindgen]
111impl WasmSparsifier {
112    /// Create a new sparsifier with the given JSON configuration.
113    ///
114    /// Pass `"{}"` or `default_config()` for defaults.
115    #[wasm_bindgen(constructor)]
116    pub fn new(config_json: &str) -> Result<WasmSparsifier, JsValue> {
117        let config: SparsifierConfig = serde_json::from_str(config_json)
118            .map_err(|e| JsValue::from_str(&format!("invalid config: {e}")))?;
119        Ok(Self {
120            inner: AdaptiveGeoSpar::new(config),
121        })
122    }
123
124    /// Build a sparsifier from a JSON edge list: `[[u, v, weight], ...]`.
125    #[wasm_bindgen(js_name = buildFromEdges)]
126    pub fn build_from_edges(
127        edges_json: &str,
128        config_json: &str,
129    ) -> Result<WasmSparsifier, JsValue> {
130        let edges: Vec<(usize, usize, f64)> = serde_json::from_str(edges_json)
131            .map_err(|e| JsValue::from_str(&format!("invalid edges: {e}")))?;
132        let config: SparsifierConfig = serde_json::from_str(config_json)
133            .map_err(|e| JsValue::from_str(&format!("invalid config: {e}")))?;
134
135        let graph = SparseGraph::from_edges(&edges);
136        let spar = AdaptiveGeoSpar::build(&graph, config)
137            .map_err(|e| JsValue::from_str(&e.to_string()))?;
138
139        Ok(Self { inner: spar })
140    }
141
142    /// Insert an edge into the full graph and update the sparsifier.
143    #[wasm_bindgen(js_name = insertEdge)]
144    pub fn insert_edge(&mut self, u: u32, v: u32, weight: f64) -> Result<(), JsValue> {
145        self.inner
146            .insert_edge(u as usize, v as usize, weight)
147            .map_err(|e| JsValue::from_str(&e.to_string()))
148    }
149
150    /// Delete an edge from the full graph and update the sparsifier.
151    #[wasm_bindgen(js_name = deleteEdge)]
152    pub fn delete_edge(&mut self, u: u32, v: u32) -> Result<(), JsValue> {
153        self.inner
154            .delete_edge(u as usize, v as usize)
155            .map_err(|e| JsValue::from_str(&e.to_string()))
156    }
157
158    /// Handle an embedding move for a node (JSON: `[[neighbor, weight], ...]`).
159    #[wasm_bindgen(js_name = updateEmbedding)]
160    pub fn update_embedding(
161        &mut self,
162        node: u32,
163        old_neighbors_json: &str,
164        new_neighbors_json: &str,
165    ) -> Result<(), JsValue> {
166        let old: Vec<(usize, f64)> = serde_json::from_str(old_neighbors_json)
167            .map_err(|e| JsValue::from_str(&format!("invalid old_neighbors: {e}")))?;
168        let new: Vec<(usize, f64)> = serde_json::from_str(new_neighbors_json)
169            .map_err(|e| JsValue::from_str(&format!("invalid new_neighbors: {e}")))?;
170
171        self.inner
172            .update_embedding(node as usize, &old, &new)
173            .map_err(|e| JsValue::from_str(&e.to_string()))
174    }
175
176    /// Run a spectral audit and return the result as JSON.
177    pub fn audit(&self) -> String {
178        let result = self.inner.audit();
179        serde_json::to_string(&result).unwrap_or_default()
180    }
181
182    /// Get the current sparsifier edges as JSON.
183    #[wasm_bindgen(js_name = sparsifierEdges)]
184    pub fn sparsifier_edges(&self) -> String {
185        let edges: Vec<(usize, usize, f64)> = self.inner.sparsifier().edges().collect();
186        serde_json::to_string(&edges).unwrap_or_default()
187    }
188
189    /// Get the statistics as JSON.
190    pub fn stats(&self) -> String {
191        serde_json::to_string(self.inner.stats()).unwrap_or_default()
192    }
193
194    /// Get the current compression ratio.
195    #[wasm_bindgen(js_name = compressionRatio)]
196    pub fn compression_ratio(&self) -> f64 {
197        self.inner.compression_ratio()
198    }
199
200    /// Rebuild the sparsifier around specific nodes (JSON array of u32).
201    #[wasm_bindgen(js_name = rebuildLocal)]
202    pub fn rebuild_local(&mut self, nodes_json: &str) -> Result<(), JsValue> {
203        let nodes: Vec<usize> = serde_json::from_str(nodes_json)
204            .map_err(|e| JsValue::from_str(&format!("invalid nodes: {e}")))?;
205        self.inner
206            .rebuild_local(&nodes)
207            .map_err(|e| JsValue::from_str(&e.to_string()))
208    }
209
210    /// Full reconstruction of the sparsifier.
211    #[wasm_bindgen(js_name = rebuildFull)]
212    pub fn rebuild_full(&mut self) -> Result<(), JsValue> {
213        self.inner
214            .rebuild_full()
215            .map_err(|e| JsValue::from_str(&e.to_string()))
216    }
217
218    /// Number of vertices in the full graph.
219    #[wasm_bindgen(js_name = numVertices)]
220    pub fn num_vertices(&self) -> u32 {
221        self.inner.full_graph().num_vertices() as u32
222    }
223
224    /// Number of edges in the full graph.
225    #[wasm_bindgen(js_name = numEdges)]
226    pub fn num_edges(&self) -> u32 {
227        self.inner.full_graph().num_edges() as u32
228    }
229
230    /// Number of edges in the sparsifier.
231    #[wasm_bindgen(js_name = sparsifierNumEdges)]
232    pub fn sparsifier_num_edges(&self) -> u32 {
233        self.inner.sparsifier().num_edges() as u32
234    }
235}