Skip to main content

ruvector_mincut_wasm/
lib.rs

1//! WASM bindings for RuVector MinCut
2//!
3//! Provides JavaScript/TypeScript API for dynamic minimum cut operations,
4//! including paper algorithms from arXiv:2512.13105.
5//!
6//! ## Features
7//!
8//! - **WasmMinCut**: Basic dynamic minimum cut (insert/delete/query)
9//! - **WasmThreeLevelHierarchy**: 3-level decomposition (Expander→Precluster→Cluster)
10//! - **WasmLocalKCut**: Deterministic local k-cut discovery with 4-color coding
11//! - **WasmMinCutWrapper**: Full API with connectivity curve analysis
12//!
13//! ## Example Usage
14//!
15//! ```javascript
16//! import init, { WasmMinCut, WasmThreeLevelHierarchy, WasmLocalKCut } from './ruvector_mincut_wasm';
17//!
18//! await init();
19//!
20//! // Basic min-cut
21//! const mincut = WasmMinCut.fromEdges([[0, 1, 1.0], [1, 2, 1.0], [0, 2, 1.0]]);
22//! console.log(mincut.minCutValue());
23//!
24//! // 3-level hierarchy decomposition
25//! const hierarchy = new WasmThreeLevelHierarchy();
26//! hierarchy.insertEdge(0, 1, 1.0);
27//! hierarchy.insertEdge(1, 2, 1.0);
28//! hierarchy.build();
29//! console.log(hierarchy.stats());
30//!
31//! // Local k-cut discovery
32//! const lkcut = new WasmLocalKCut(5, 100, 2);
33//! lkcut.insertEdge(0, 1, 1.0);
34//! const cuts = lkcut.query(0);
35//! ```
36
37use ruvector_mincut::cluster::hierarchy::{HierarchyConfig, ThreeLevelHierarchy};
38use ruvector_mincut::localkcut::deterministic::DeterministicLocalKCut;
39use ruvector_mincut::{DynamicGraph, DynamicMinCut, MinCutBuilder, MinCutConfig, MinCutWrapper};
40use serde::{Deserialize, Serialize};
41use std::sync::Arc;
42use wasm_bindgen::prelude::*;
43
44/// WASM wrapper for DynamicMinCut
45#[wasm_bindgen]
46pub struct WasmMinCut {
47    inner: DynamicMinCut,
48}
49
50#[derive(Serialize, Deserialize)]
51struct EdgeInput {
52    u: u64,
53    v: u64,
54    weight: f64,
55}
56
57#[derive(Serialize, Deserialize)]
58struct Partition {
59    s: Vec<u64>,
60    t: Vec<u64>,
61}
62
63#[derive(Serialize, Deserialize)]
64struct Edge {
65    u: u64,
66    v: u64,
67    weight: f64,
68}
69
70#[derive(Serialize, Deserialize)]
71struct Stats {
72    num_vertices: usize,
73    num_edges: usize,
74    min_cut_value: f64,
75    is_connected: bool,
76    num_operations: usize,
77}
78
79#[wasm_bindgen]
80impl WasmMinCut {
81    /// Create a new empty minimum cut structure
82    #[wasm_bindgen(constructor)]
83    pub fn new() -> Result<WasmMinCut, JsError> {
84        console_error_panic_hook::set_once();
85
86        Ok(WasmMinCut {
87            inner: DynamicMinCut::new(MinCutConfig::default()),
88        })
89    }
90
91    /// Create from edges array: [[u, v, weight], ...]
92    ///
93    /// # Arguments
94    /// * `edges` - JavaScript array of [u, v, weight] tuples
95    ///
96    /// # Example
97    /// ```javascript
98    /// const edges = [[0, 1, 1.5], [1, 2, 2.0]];
99    /// const mincut = WasmMinCut.fromEdges(edges);
100    /// ```
101    #[wasm_bindgen(js_name = "fromEdges")]
102    pub fn from_edges(edges: JsValue) -> Result<WasmMinCut, JsError> {
103        console_error_panic_hook::set_once();
104
105        // Deserialize edges from JavaScript array
106        let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
107            .map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
108
109        // Convert to tuple format expected by with_edges
110        let mut edge_tuples = Vec::with_capacity(edges_vec.len());
111
112        for edge in edges_vec {
113            if edge.len() != 3 {
114                return Err(JsError::new("Each edge must be [u, v, weight]"));
115            }
116
117            let u = edge[0] as u64;
118            let v = edge[1] as u64;
119            let weight = edge[2];
120
121            edge_tuples.push((u, v, weight));
122        }
123
124        let inner = MinCutBuilder::new()
125            .with_edges(edge_tuples)
126            .build()
127            .map_err(|e| JsError::new(&format!("Failed to build mincut: {}", e)))?;
128
129        Ok(WasmMinCut { inner })
130    }
131
132    /// Insert an edge into the graph
133    ///
134    /// # Arguments
135    /// * `u` - Source vertex
136    /// * `v` - Target vertex
137    /// * `weight` - Edge weight
138    ///
139    /// # Returns
140    /// The new minimum cut value after insertion
141    #[wasm_bindgen(js_name = "insertEdge")]
142    pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) -> Result<f64, JsError> {
143        self.inner
144            .insert_edge(u, v, weight)
145            .map_err(|e| JsError::new(&format!("Failed to insert edge: {}", e)))
146    }
147
148    /// Delete an edge from the graph
149    ///
150    /// # Arguments
151    /// * `u` - Source vertex
152    /// * `v` - Target vertex
153    ///
154    /// # Returns
155    /// The new minimum cut value after deletion
156    #[wasm_bindgen(js_name = "deleteEdge")]
157    pub fn delete_edge(&mut self, u: u64, v: u64) -> Result<f64, JsError> {
158        self.inner
159            .delete_edge(u, v)
160            .map_err(|e| JsError::new(&format!("Failed to delete edge: {}", e)))
161    }
162
163    /// Get the current minimum cut value
164    ///
165    /// # Returns
166    /// The sum of edge weights in the minimum cut
167    #[wasm_bindgen(js_name = "minCutValue")]
168    pub fn min_cut_value(&self) -> f64 {
169        self.inner.min_cut_value()
170    }
171
172    /// Get the partition as JSON: { "s": [...], "t": [...] }
173    ///
174    /// # Returns
175    /// JavaScript object with two arrays: `s` and `t` containing vertex IDs
176    ///
177    /// # Example
178    /// ```javascript
179    /// const { s, t } = mincut.partition();
180    /// console.log("S partition:", s);
181    /// console.log("T partition:", t);
182    /// ```
183    #[wasm_bindgen]
184    pub fn partition(&self) -> JsValue {
185        let (s_set, t_set) = self.inner.partition();
186
187        let partition = Partition {
188            s: s_set.into_iter().collect(),
189            t: t_set.into_iter().collect(),
190        };
191
192        serde_wasm_bindgen::to_value(&partition).unwrap_or(JsValue::NULL)
193    }
194
195    /// Get the cut edges as JSON array
196    ///
197    /// # Returns
198    /// JavaScript array of edge objects: [{ u, v, weight }, ...]
199    ///
200    /// # Example
201    /// ```javascript
202    /// const edges = mincut.cutEdges();
203    /// edges.forEach(e => console.log(`Edge ${e.u}-${e.v}: ${e.weight}`));
204    /// ```
205    #[wasm_bindgen(js_name = "cutEdges")]
206    pub fn cut_edges(&self) -> JsValue {
207        let edges = self.inner.cut_edges();
208
209        let edge_list: Vec<Edge> = edges
210            .into_iter()
211            .map(|e| Edge {
212                u: e.source,
213                v: e.target,
214                weight: e.weight,
215            })
216            .collect();
217
218        serde_wasm_bindgen::to_value(&edge_list).unwrap_or(JsValue::NULL)
219    }
220
221    /// Get the number of vertices in the graph
222    #[wasm_bindgen(js_name = "numVertices")]
223    pub fn num_vertices(&self) -> usize {
224        self.inner.num_vertices()
225    }
226
227    /// Get the number of edges in the graph
228    #[wasm_bindgen(js_name = "numEdges")]
229    pub fn num_edges(&self) -> usize {
230        self.inner.num_edges()
231    }
232
233    /// Check if the graph is connected
234    ///
235    /// # Returns
236    /// `true` if there is a path between all vertex pairs
237    #[wasm_bindgen(js_name = "isConnected")]
238    pub fn is_connected(&self) -> bool {
239        self.inner.is_connected()
240    }
241
242    /// Get comprehensive statistics as JSON
243    ///
244    /// # Returns
245    /// JavaScript object with:
246    /// - `num_vertices`: Number of vertices
247    /// - `num_edges`: Number of edges
248    /// - `min_cut_value`: Current minimum cut value
249    /// - `is_connected`: Whether graph is connected
250    /// - `num_operations`: Total operations performed
251    ///
252    /// # Example
253    /// ```javascript
254    /// const stats = mincut.stats();
255    /// console.log(`Graph has ${stats.num_vertices} vertices and ${stats.num_edges} edges`);
256    /// console.log(`Minimum cut value: ${stats.min_cut_value}`);
257    /// ```
258    #[wasm_bindgen]
259    pub fn stats(&self) -> JsValue {
260        let algo_stats = self.inner.stats();
261        let stats = Stats {
262            num_vertices: self.inner.num_vertices(),
263            num_edges: self.inner.num_edges(),
264            min_cut_value: self.inner.min_cut_value(),
265            is_connected: self.inner.is_connected(),
266            num_operations: (algo_stats.insertions + algo_stats.deletions + algo_stats.queries)
267                as usize,
268        };
269
270        serde_wasm_bindgen::to_value(&stats).unwrap_or(JsValue::NULL)
271    }
272
273    /// Update an edge weight (delete old, insert new)
274    ///
275    /// # Arguments
276    /// * `u` - Source vertex
277    /// * `v` - Target vertex
278    /// * `new_weight` - New edge weight
279    ///
280    /// # Returns
281    /// The new minimum cut value after update
282    #[wasm_bindgen(js_name = "updateEdge")]
283    pub fn update_edge(&mut self, u: u64, v: u64, new_weight: f64) -> Result<f64, JsError> {
284        // Delete old edge (ignore error if doesn't exist)
285        let _ = self.inner.delete_edge(u, v);
286
287        // Insert with new weight
288        self.inner
289            .insert_edge(u, v, new_weight)
290            .map_err(|e| JsError::new(&format!("Failed to update edge: {}", e)))
291    }
292
293    /// Batch insert multiple edges
294    ///
295    /// # Arguments
296    /// * `edges` - JavaScript array of [u, v, weight] tuples
297    ///
298    /// # Returns
299    /// The final minimum cut value
300    ///
301    /// # Example
302    /// ```javascript
303    /// const edges = [[0, 1, 1.0], [1, 2, 2.0], [2, 3, 1.5]];
304    /// const cutValue = mincut.batchInsert(edges);
305    /// ```
306    #[wasm_bindgen(js_name = "batchInsert")]
307    pub fn batch_insert(&mut self, edges: JsValue) -> Result<f64, JsError> {
308        let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
309            .map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
310
311        for edge in edges_vec {
312            if edge.len() != 3 {
313                return Err(JsError::new("Each edge must be [u, v, weight]"));
314            }
315
316            let u = edge[0] as u64;
317            let v = edge[1] as u64;
318            let weight = edge[2];
319
320            self.inner.insert_edge(u, v, weight).map_err(|e| {
321                JsError::new(&format!("Failed to insert edge [{}, {}]: {}", u, v, e))
322            })?;
323        }
324
325        Ok(self.inner.min_cut_value())
326    }
327
328    /// Batch delete multiple edges
329    ///
330    /// # Arguments
331    /// * `edges` - JavaScript array of [u, v] tuples
332    ///
333    /// # Returns
334    /// The final minimum cut value
335    ///
336    /// # Example
337    /// ```javascript
338    /// const edges = [[0, 1], [1, 2]];
339    /// const cutValue = mincut.batchDelete(edges);
340    /// ```
341    #[wasm_bindgen(js_name = "batchDelete")]
342    pub fn batch_delete(&mut self, edges: JsValue) -> Result<f64, JsError> {
343        let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
344            .map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
345
346        for edge in edges_vec {
347            if edge.len() < 2 {
348                return Err(JsError::new("Each edge must be [u, v] or [u, v, weight]"));
349            }
350
351            let u = edge[0] as u64;
352            let v = edge[1] as u64;
353
354            self.inner.delete_edge(u, v).map_err(|e| {
355                JsError::new(&format!("Failed to delete edge [{}, {}]: {}", u, v, e))
356            })?;
357        }
358
359        Ok(self.inner.min_cut_value())
360    }
361
362    /// Clear all edges from the graph
363    #[wasm_bindgen]
364    pub fn clear(&mut self) {
365        self.inner = DynamicMinCut::new(MinCutConfig::default());
366    }
367}
368
369/// Initialize the WASM module (call once at startup)
370///
371/// This sets up panic hooks for better error messages in the browser console.
372#[wasm_bindgen(start)]
373pub fn init() {
374    console_error_panic_hook::set_once();
375}
376
377/// Get version information
378#[wasm_bindgen(js_name = "getVersion")]
379pub fn get_version() -> String {
380    env!("CARGO_PKG_VERSION").to_string()
381}
382
383// ============================================================================
384// ThreeLevelHierarchy - Paper Section 3: Expander → Precluster → Cluster
385// ============================================================================
386
387/// Statistics for the hierarchy
388#[derive(Serialize, Deserialize)]
389struct HierarchyStatsJs {
390    num_expanders: usize,
391    num_preclusters: usize,
392    num_clusters: usize,
393    num_vertices: usize,
394    num_edges: usize,
395    global_min_cut: f64,
396    avg_expander_size: f64,
397}
398
399/// WASM wrapper for ThreeLevelHierarchy
400///
401/// Implements the 3-level decomposition from arXiv:2512.13105:
402/// - Level 0: Expanders (φ-expander subgraphs)
403/// - Level 1: Preclusters (groups of expanders)
404/// - Level 2: Clusters (top-level grouping with mirror cuts)
405#[wasm_bindgen]
406pub struct WasmThreeLevelHierarchy {
407    inner: ThreeLevelHierarchy,
408}
409
410#[wasm_bindgen]
411impl WasmThreeLevelHierarchy {
412    /// Create a new hierarchy with default configuration
413    #[wasm_bindgen(constructor)]
414    pub fn new() -> WasmThreeLevelHierarchy {
415        console_error_panic_hook::set_once();
416        WasmThreeLevelHierarchy {
417            inner: ThreeLevelHierarchy::with_defaults(),
418        }
419    }
420
421    /// Create hierarchy with custom expansion parameter φ
422    #[wasm_bindgen(js_name = "withPhi")]
423    pub fn with_phi(phi: f64) -> WasmThreeLevelHierarchy {
424        console_error_panic_hook::set_once();
425        WasmThreeLevelHierarchy {
426            inner: ThreeLevelHierarchy::new(HierarchyConfig {
427                phi,
428                ..Default::default()
429            }),
430        }
431    }
432
433    /// Insert an edge into the graph
434    #[wasm_bindgen(js_name = "insertEdge")]
435    pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) {
436        self.inner.insert_edge(u, v, weight);
437    }
438
439    /// Delete an edge from the graph
440    #[wasm_bindgen(js_name = "deleteEdge")]
441    pub fn delete_edge(&mut self, u: u64, v: u64) {
442        self.inner.delete_edge(u, v);
443    }
444
445    /// Build the complete 3-level hierarchy
446    ///
447    /// Must be called after inserting edges to compute the decomposition.
448    #[wasm_bindgen]
449    pub fn build(&mut self) {
450        self.inner.build();
451    }
452
453    /// Get hierarchy statistics as JSON
454    #[wasm_bindgen]
455    pub fn stats(&self) -> JsValue {
456        let s = self.inner.stats();
457        let stats = HierarchyStatsJs {
458            num_expanders: s.num_expanders,
459            num_preclusters: s.num_preclusters,
460            num_clusters: s.num_clusters,
461            num_vertices: s.num_vertices,
462            num_edges: s.num_edges,
463            global_min_cut: s.global_min_cut,
464            avg_expander_size: s.avg_expander_size,
465        };
466        serde_wasm_bindgen::to_value(&stats).unwrap_or(JsValue::NULL)
467    }
468
469    /// Get the global minimum cut estimate
470    #[wasm_bindgen(js_name = "globalMinCut")]
471    pub fn global_min_cut(&self) -> f64 {
472        self.inner.global_min_cut
473    }
474
475    /// Get all vertices as JSON array
476    #[wasm_bindgen]
477    pub fn vertices(&self) -> JsValue {
478        let verts: Vec<u64> = self.inner.vertices();
479        serde_wasm_bindgen::to_value(&verts).unwrap_or(JsValue::NULL)
480    }
481}
482
483// ============================================================================
484// DeterministicLocalKCut - Paper Theorem 4.1: Color-coded DFS
485// ============================================================================
486
487/// Local cut result for JS
488#[derive(Serialize, Deserialize)]
489struct LocalCutJs {
490    cut_value: f64,
491    vertices: Vec<u64>,
492}
493
494/// WASM wrapper for DeterministicLocalKCut
495///
496/// Implements the deterministic local k-cut algorithm from arXiv:2512.13105:
497/// - Uses 4-color coding (red-blue, green-yellow)
498/// - Greedy forest packing for edge classification
499/// - Color-coded DFS for cut enumeration
500#[wasm_bindgen]
501pub struct WasmLocalKCut {
502    inner: DeterministicLocalKCut,
503    num_vertices: usize,
504    num_edges: usize,
505}
506
507#[wasm_bindgen]
508impl WasmLocalKCut {
509    /// Create a new LocalKCut structure
510    ///
511    /// # Arguments
512    /// * `lambda_max` - Maximum cut value to consider
513    /// * `volume_bound` - Maximum volume to explore (nu parameter)
514    /// * `beta` - Cut depth parameter (typically 2)
515    #[wasm_bindgen(constructor)]
516    pub fn new(lambda_max: u64, volume_bound: usize, beta: usize) -> WasmLocalKCut {
517        console_error_panic_hook::set_once();
518        WasmLocalKCut {
519            inner: DeterministicLocalKCut::new(lambda_max, volume_bound, beta),
520            num_vertices: 0,
521            num_edges: 0,
522        }
523    }
524
525    /// Insert an edge
526    #[wasm_bindgen(js_name = "insertEdge")]
527    pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) {
528        self.inner.insert_edge(u, v, weight);
529        self.num_edges += 1;
530        // Rough vertex count estimate (may overcount)
531        self.num_vertices = self.num_vertices.max((u.max(v) + 1) as usize);
532    }
533
534    /// Delete an edge
535    #[wasm_bindgen(js_name = "deleteEdge")]
536    pub fn delete_edge(&mut self, u: u64, v: u64) {
537        self.inner.delete_edge(u, v);
538        self.num_edges = self.num_edges.saturating_sub(1);
539    }
540
541    /// Query local cuts from a source vertex
542    ///
543    /// Returns array of { cut_value, vertices } objects
544    #[wasm_bindgen]
545    pub fn query(&self, source: u64) -> JsValue {
546        let results = self.inner.query(source);
547        let cuts: Vec<LocalCutJs> = results
548            .into_iter()
549            .map(|c| LocalCutJs {
550                cut_value: c.cut_value,
551                vertices: c.vertices.into_iter().collect(),
552            })
553            .collect();
554        serde_wasm_bindgen::to_value(&cuts).unwrap_or(JsValue::NULL)
555    }
556
557    /// Get number of vertices (approximate)
558    #[wasm_bindgen(js_name = "numVertices")]
559    pub fn num_vertices(&self) -> usize {
560        self.num_vertices
561    }
562
563    /// Get number of edges
564    #[wasm_bindgen(js_name = "numEdges")]
565    pub fn num_edges(&self) -> usize {
566        self.num_edges
567    }
568}
569
570// ============================================================================
571// MinCutWrapper - Full API with Connectivity Curve Analysis
572// ============================================================================
573
574/// Connectivity curve point
575#[derive(Serialize, Deserialize)]
576struct CurvePoint {
577    k: usize,
578    min_cut: u64,
579}
580
581/// Elbow detection result
582#[derive(Serialize, Deserialize)]
583struct ElbowResult {
584    k: usize,
585    drop: u64,
586}
587
588/// WASM wrapper for MinCutWrapper
589///
590/// High-level API combining all paper algorithms:
591/// - O(log n) instance management
592/// - ThreeLevelHierarchy decomposition
593/// - LocalKCut discovery
594/// - Connectivity curve analysis for boundary validation
595#[wasm_bindgen]
596pub struct WasmMinCutWrapper {
597    inner: MinCutWrapper,
598}
599
600#[wasm_bindgen]
601impl WasmMinCutWrapper {
602    /// Create a new MinCutWrapper
603    #[wasm_bindgen(constructor)]
604    pub fn new() -> WasmMinCutWrapper {
605        console_error_panic_hook::set_once();
606        let graph = Arc::new(DynamicGraph::new());
607        WasmMinCutWrapper {
608            inner: MinCutWrapper::new(graph),
609        }
610    }
611
612    /// Insert an edge (timestamp auto-incremented)
613    #[wasm_bindgen(js_name = "insertEdge")]
614    pub fn insert_edge(&mut self, u: u64, v: u64) {
615        let time = self.inner.current_time() + 1;
616        self.inner.insert_edge(time, u, v);
617    }
618
619    /// Delete an edge (timestamp auto-incremented)
620    #[wasm_bindgen(js_name = "deleteEdge")]
621    pub fn delete_edge(&mut self, u: u64, v: u64) {
622        let time = self.inner.current_time() + 1;
623        self.inner.delete_edge(time, u, v);
624    }
625
626    /// Query the minimum cut value
627    #[wasm_bindgen]
628    pub fn query(&mut self) -> f64 {
629        self.inner.min_cut_value() as f64
630    }
631
632    /// Get number of active instances
633    #[wasm_bindgen(js_name = "numInstances")]
634    pub fn num_instances(&self) -> usize {
635        self.inner.num_instances()
636    }
637
638    /// Get current logical time
639    #[wasm_bindgen(js_name = "currentTime")]
640    pub fn current_time(&self) -> u64 {
641        self.inner.current_time()
642    }
643
644    /// Query with LocalKCut certification
645    ///
646    /// Returns { cut_value, certified } object
647    #[wasm_bindgen(js_name = "queryWithCertification")]
648    pub fn query_with_certification(&mut self, source: u64) -> JsValue {
649        let (cut_value, certified) = self.inner.query_with_local_kcut(source);
650        let result = serde_json::json!({
651            "cut_value": cut_value,
652            "certified": certified,
653        });
654        serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
655    }
656
657    /// Get local cuts from a source vertex
658    #[wasm_bindgen(js_name = "localCuts")]
659    pub fn local_cuts(&self, source: u64, lambda_max: u64) -> JsValue {
660        let cuts = self.inner.local_cuts(source, lambda_max);
661        let result: Vec<LocalCutJs> = cuts
662            .into_iter()
663            .map(|(value, verts)| LocalCutJs {
664                cut_value: value,
665                vertices: verts,
666            })
667            .collect();
668        serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
669    }
670
671    /// Compute edge-connectivity degradation curve
672    ///
673    /// # Arguments
674    /// * `ranked_edges` - Array of [u, v, score] ranked by cut-likelihood
675    /// * `k_max` - Maximum edges to remove
676    ///
677    /// # Returns
678    /// Array of { k, min_cut } showing degradation
679    #[wasm_bindgen(js_name = "connectivityCurve")]
680    pub fn connectivity_curve(&self, ranked_edges: JsValue, k_max: usize) -> JsValue {
681        let edges: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(ranked_edges).unwrap_or_default();
682
683        let ranked: Vec<(u64, u64, f64)> = edges
684            .into_iter()
685            .filter_map(|e| {
686                if e.len() >= 3 {
687                    Some((e[0] as u64, e[1] as u64, e[2]))
688                } else {
689                    None
690                }
691            })
692            .collect();
693
694        let curve = self.inner.connectivity_curve(&ranked, k_max);
695        let result: Vec<CurvePoint> = curve
696            .into_iter()
697            .map(|(k, min_cut)| CurvePoint { k, min_cut })
698            .collect();
699
700        serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
701    }
702
703    /// Find elbow point in connectivity curve
704    ///
705    /// Returns { k, drop } or null if no elbow found
706    #[wasm_bindgen(js_name = "findElbow")]
707    pub fn find_elbow(curve: JsValue) -> JsValue {
708        let points: Vec<CurvePoint> = serde_wasm_bindgen::from_value(curve).unwrap_or_default();
709
710        let curve_data: Vec<(usize, u64)> = points.into_iter().map(|p| (p.k, p.min_cut)).collect();
711
712        match MinCutWrapper::find_elbow(&curve_data) {
713            Some((k, drop)) => {
714                let result = ElbowResult { k, drop };
715                serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
716            }
717            None => JsValue::NULL,
718        }
719    }
720
721    /// Compute detector quality score
722    ///
723    /// # Arguments
724    /// * `ranked_edges` - Array of [u, v, score]
725    /// * `true_cut_size` - Known size of true minimum cut
726    ///
727    /// # Returns
728    /// Quality score from 0.0 (poor) to 1.0 (perfect)
729    #[wasm_bindgen(js_name = "detectorQuality")]
730    pub fn detector_quality(&self, ranked_edges: JsValue, true_cut_size: usize) -> f64 {
731        let edges: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(ranked_edges).unwrap_or_default();
732
733        let ranked: Vec<(u64, u64, f64)> = edges
734            .into_iter()
735            .filter_map(|e| {
736                if e.len() >= 3 {
737                    Some((e[0] as u64, e[1] as u64, e[2]))
738                } else {
739                    None
740                }
741            })
742            .collect();
743
744        self.inner.detector_quality(&ranked, true_cut_size)
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751    use wasm_bindgen_test::*;
752
753    #[wasm_bindgen_test]
754    fn test_new() {
755        let mincut = WasmMinCut::new().unwrap();
756        assert_eq!(mincut.num_vertices(), 0);
757        assert_eq!(mincut.num_edges(), 0);
758    }
759
760    #[wasm_bindgen_test]
761    fn test_insert_edge() {
762        let mut mincut = WasmMinCut::new().unwrap();
763        let result = mincut.insert_edge(0, 1, 1.0);
764        assert!(result.is_ok());
765        assert_eq!(mincut.num_edges(), 1);
766    }
767
768    #[wasm_bindgen_test]
769    fn test_min_cut_value() {
770        let mut mincut = WasmMinCut::new().unwrap();
771        mincut.insert_edge(0, 1, 1.0).unwrap();
772        mincut.insert_edge(1, 2, 2.0).unwrap();
773        mincut.insert_edge(0, 2, 1.5).unwrap();
774
775        let cut_value = mincut.min_cut_value();
776        assert!(cut_value > 0.0);
777    }
778}