Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
31
vendor/ruvector/crates/ruvector-mincut-wasm/Cargo.toml
vendored
Normal file
31
vendor/ruvector/crates/ruvector-mincut-wasm/Cargo.toml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "ruvector-mincut-wasm"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "WASM bindings for subpolynomial-time dynamic minimum cut"
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
keywords = ["mincut", "wasm", "graph", "algorithm", "dynamic"]
|
||||
categories = ["wasm", "algorithms"]
|
||||
readme = "README.md"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
ruvector-mincut = { version = "2.0", path = "../ruvector-mincut", default-features = false, features = ["wasm"] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde-wasm-bindgen = "0.6"
|
||||
console_error_panic_hook = "0.1"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = ["-O4"]
|
||||
41
vendor/ruvector/crates/ruvector-mincut-wasm/README.md
vendored
Normal file
41
vendor/ruvector/crates/ruvector-mincut-wasm/README.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# ruvector-mincut-wasm
|
||||
|
||||
WebAssembly bindings for [ruvector-mincut](https://crates.io/crates/ruvector-mincut) - the world's first subpolynomial-time dynamic minimum cut implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Browser & Node.js**: Works in any JavaScript environment with WASM support
|
||||
- **Full API**: Complete access to dynamic mincut operations
|
||||
- **Zero Dependencies**: Pure WASM, no runtime requirements
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install ruvector-mincut-wasm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```javascript
|
||||
import init, { DynamicMinCut } from 'ruvector-mincut-wasm';
|
||||
|
||||
await init();
|
||||
const graph = new DynamicMinCut(100);
|
||||
graph.addEdge(0, 1, 1.0);
|
||||
const mincut = graph.computeMinCut();
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- O(n^{1-ε}) query time for dynamic minimum cut
|
||||
- Matches theoretical lower bounds
|
||||
- SIMD-optimized when available
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## See Also
|
||||
|
||||
- [ruvector-mincut](https://crates.io/crates/ruvector-mincut) - Core Rust implementation
|
||||
- [ruvector-mincut-node](https://crates.io/crates/ruvector-mincut-node) - Node.js native bindings
|
||||
778
vendor/ruvector/crates/ruvector-mincut-wasm/src/lib.rs
vendored
Normal file
778
vendor/ruvector/crates/ruvector-mincut-wasm/src/lib.rs
vendored
Normal file
@@ -0,0 +1,778 @@
|
||||
//! WASM bindings for RuVector MinCut
|
||||
//!
|
||||
//! Provides JavaScript/TypeScript API for dynamic minimum cut operations,
|
||||
//! including paper algorithms from arXiv:2512.13105.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **WasmMinCut**: Basic dynamic minimum cut (insert/delete/query)
|
||||
//! - **WasmThreeLevelHierarchy**: 3-level decomposition (Expander→Precluster→Cluster)
|
||||
//! - **WasmLocalKCut**: Deterministic local k-cut discovery with 4-color coding
|
||||
//! - **WasmMinCutWrapper**: Full API with connectivity curve analysis
|
||||
//!
|
||||
//! ## Example Usage
|
||||
//!
|
||||
//! ```javascript
|
||||
//! import init, { WasmMinCut, WasmThreeLevelHierarchy, WasmLocalKCut } from './ruvector_mincut_wasm';
|
||||
//!
|
||||
//! await init();
|
||||
//!
|
||||
//! // Basic min-cut
|
||||
//! const mincut = WasmMinCut.fromEdges([[0, 1, 1.0], [1, 2, 1.0], [0, 2, 1.0]]);
|
||||
//! console.log(mincut.minCutValue());
|
||||
//!
|
||||
//! // 3-level hierarchy decomposition
|
||||
//! const hierarchy = new WasmThreeLevelHierarchy();
|
||||
//! hierarchy.insertEdge(0, 1, 1.0);
|
||||
//! hierarchy.insertEdge(1, 2, 1.0);
|
||||
//! hierarchy.build();
|
||||
//! console.log(hierarchy.stats());
|
||||
//!
|
||||
//! // Local k-cut discovery
|
||||
//! const lkcut = new WasmLocalKCut(5, 100, 2);
|
||||
//! lkcut.insertEdge(0, 1, 1.0);
|
||||
//! const cuts = lkcut.query(0);
|
||||
//! ```
|
||||
|
||||
use ruvector_mincut::cluster::hierarchy::{HierarchyConfig, ThreeLevelHierarchy};
|
||||
use ruvector_mincut::localkcut::deterministic::DeterministicLocalKCut;
|
||||
use ruvector_mincut::{DynamicGraph, DynamicMinCut, MinCutBuilder, MinCutConfig, MinCutWrapper};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// WASM wrapper for DynamicMinCut
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmMinCut {
|
||||
inner: DynamicMinCut,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct EdgeInput {
|
||||
u: u64,
|
||||
v: u64,
|
||||
weight: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Partition {
|
||||
s: Vec<u64>,
|
||||
t: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Edge {
|
||||
u: u64,
|
||||
v: u64,
|
||||
weight: f64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Stats {
|
||||
num_vertices: usize,
|
||||
num_edges: usize,
|
||||
min_cut_value: f64,
|
||||
is_connected: bool,
|
||||
num_operations: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMinCut {
|
||||
/// Create a new empty minimum cut structure
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> Result<WasmMinCut, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
Ok(WasmMinCut {
|
||||
inner: DynamicMinCut::new(MinCutConfig::default()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from edges array: [[u, v, weight], ...]
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `edges` - JavaScript array of [u, v, weight] tuples
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const edges = [[0, 1, 1.5], [1, 2, 2.0]];
|
||||
/// const mincut = WasmMinCut.fromEdges(edges);
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "fromEdges")]
|
||||
pub fn from_edges(edges: JsValue) -> Result<WasmMinCut, JsError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
// Deserialize edges from JavaScript array
|
||||
let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
|
||||
.map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
|
||||
|
||||
// Convert to tuple format expected by with_edges
|
||||
let mut edge_tuples = Vec::with_capacity(edges_vec.len());
|
||||
|
||||
for edge in edges_vec {
|
||||
if edge.len() != 3 {
|
||||
return Err(JsError::new("Each edge must be [u, v, weight]"));
|
||||
}
|
||||
|
||||
let u = edge[0] as u64;
|
||||
let v = edge[1] as u64;
|
||||
let weight = edge[2];
|
||||
|
||||
edge_tuples.push((u, v, weight));
|
||||
}
|
||||
|
||||
let inner = MinCutBuilder::new()
|
||||
.with_edges(edge_tuples)
|
||||
.build()
|
||||
.map_err(|e| JsError::new(&format!("Failed to build mincut: {}", e)))?;
|
||||
|
||||
Ok(WasmMinCut { inner })
|
||||
}
|
||||
|
||||
/// Insert an edge into the graph
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `u` - Source vertex
|
||||
/// * `v` - Target vertex
|
||||
/// * `weight` - Edge weight
|
||||
///
|
||||
/// # Returns
|
||||
/// The new minimum cut value after insertion
|
||||
#[wasm_bindgen(js_name = "insertEdge")]
|
||||
pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) -> Result<f64, JsError> {
|
||||
self.inner
|
||||
.insert_edge(u, v, weight)
|
||||
.map_err(|e| JsError::new(&format!("Failed to insert edge: {}", e)))
|
||||
}
|
||||
|
||||
/// Delete an edge from the graph
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `u` - Source vertex
|
||||
/// * `v` - Target vertex
|
||||
///
|
||||
/// # Returns
|
||||
/// The new minimum cut value after deletion
|
||||
#[wasm_bindgen(js_name = "deleteEdge")]
|
||||
pub fn delete_edge(&mut self, u: u64, v: u64) -> Result<f64, JsError> {
|
||||
self.inner
|
||||
.delete_edge(u, v)
|
||||
.map_err(|e| JsError::new(&format!("Failed to delete edge: {}", e)))
|
||||
}
|
||||
|
||||
/// Get the current minimum cut value
|
||||
///
|
||||
/// # Returns
|
||||
/// The sum of edge weights in the minimum cut
|
||||
#[wasm_bindgen(js_name = "minCutValue")]
|
||||
pub fn min_cut_value(&self) -> f64 {
|
||||
self.inner.min_cut_value()
|
||||
}
|
||||
|
||||
/// Get the partition as JSON: { "s": [...], "t": [...] }
|
||||
///
|
||||
/// # Returns
|
||||
/// JavaScript object with two arrays: `s` and `t` containing vertex IDs
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const { s, t } = mincut.partition();
|
||||
/// console.log("S partition:", s);
|
||||
/// console.log("T partition:", t);
|
||||
/// ```
|
||||
#[wasm_bindgen]
|
||||
pub fn partition(&self) -> JsValue {
|
||||
let (s_set, t_set) = self.inner.partition();
|
||||
|
||||
let partition = Partition {
|
||||
s: s_set.into_iter().collect(),
|
||||
t: t_set.into_iter().collect(),
|
||||
};
|
||||
|
||||
serde_wasm_bindgen::to_value(&partition).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Get the cut edges as JSON array
|
||||
///
|
||||
/// # Returns
|
||||
/// JavaScript array of edge objects: [{ u, v, weight }, ...]
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const edges = mincut.cutEdges();
|
||||
/// edges.forEach(e => console.log(`Edge ${e.u}-${e.v}: ${e.weight}`));
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "cutEdges")]
|
||||
pub fn cut_edges(&self) -> JsValue {
|
||||
let edges = self.inner.cut_edges();
|
||||
|
||||
let edge_list: Vec<Edge> = edges
|
||||
.into_iter()
|
||||
.map(|e| Edge {
|
||||
u: e.source,
|
||||
v: e.target,
|
||||
weight: e.weight,
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_wasm_bindgen::to_value(&edge_list).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Get the number of vertices in the graph
|
||||
#[wasm_bindgen(js_name = "numVertices")]
|
||||
pub fn num_vertices(&self) -> usize {
|
||||
self.inner.num_vertices()
|
||||
}
|
||||
|
||||
/// Get the number of edges in the graph
|
||||
#[wasm_bindgen(js_name = "numEdges")]
|
||||
pub fn num_edges(&self) -> usize {
|
||||
self.inner.num_edges()
|
||||
}
|
||||
|
||||
/// Check if the graph is connected
|
||||
///
|
||||
/// # Returns
|
||||
/// `true` if there is a path between all vertex pairs
|
||||
#[wasm_bindgen(js_name = "isConnected")]
|
||||
pub fn is_connected(&self) -> bool {
|
||||
self.inner.is_connected()
|
||||
}
|
||||
|
||||
/// Get comprehensive statistics as JSON
|
||||
///
|
||||
/// # Returns
|
||||
/// JavaScript object with:
|
||||
/// - `num_vertices`: Number of vertices
|
||||
/// - `num_edges`: Number of edges
|
||||
/// - `min_cut_value`: Current minimum cut value
|
||||
/// - `is_connected`: Whether graph is connected
|
||||
/// - `num_operations`: Total operations performed
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const stats = mincut.stats();
|
||||
/// console.log(`Graph has ${stats.num_vertices} vertices and ${stats.num_edges} edges`);
|
||||
/// console.log(`Minimum cut value: ${stats.min_cut_value}`);
|
||||
/// ```
|
||||
#[wasm_bindgen]
|
||||
pub fn stats(&self) -> JsValue {
|
||||
let algo_stats = self.inner.stats();
|
||||
let stats = Stats {
|
||||
num_vertices: self.inner.num_vertices(),
|
||||
num_edges: self.inner.num_edges(),
|
||||
min_cut_value: self.inner.min_cut_value(),
|
||||
is_connected: self.inner.is_connected(),
|
||||
num_operations: (algo_stats.insertions + algo_stats.deletions + algo_stats.queries)
|
||||
as usize,
|
||||
};
|
||||
|
||||
serde_wasm_bindgen::to_value(&stats).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Update an edge weight (delete old, insert new)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `u` - Source vertex
|
||||
/// * `v` - Target vertex
|
||||
/// * `new_weight` - New edge weight
|
||||
///
|
||||
/// # Returns
|
||||
/// The new minimum cut value after update
|
||||
#[wasm_bindgen(js_name = "updateEdge")]
|
||||
pub fn update_edge(&mut self, u: u64, v: u64, new_weight: f64) -> Result<f64, JsError> {
|
||||
// Delete old edge (ignore error if doesn't exist)
|
||||
let _ = self.inner.delete_edge(u, v);
|
||||
|
||||
// Insert with new weight
|
||||
self.inner
|
||||
.insert_edge(u, v, new_weight)
|
||||
.map_err(|e| JsError::new(&format!("Failed to update edge: {}", e)))
|
||||
}
|
||||
|
||||
/// Batch insert multiple edges
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `edges` - JavaScript array of [u, v, weight] tuples
|
||||
///
|
||||
/// # Returns
|
||||
/// The final minimum cut value
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const edges = [[0, 1, 1.0], [1, 2, 2.0], [2, 3, 1.5]];
|
||||
/// const cutValue = mincut.batchInsert(edges);
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "batchInsert")]
|
||||
pub fn batch_insert(&mut self, edges: JsValue) -> Result<f64, JsError> {
|
||||
let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
|
||||
.map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
|
||||
|
||||
for edge in edges_vec {
|
||||
if edge.len() != 3 {
|
||||
return Err(JsError::new("Each edge must be [u, v, weight]"));
|
||||
}
|
||||
|
||||
let u = edge[0] as u64;
|
||||
let v = edge[1] as u64;
|
||||
let weight = edge[2];
|
||||
|
||||
self.inner.insert_edge(u, v, weight).map_err(|e| {
|
||||
JsError::new(&format!("Failed to insert edge [{}, {}]: {}", u, v, e))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(self.inner.min_cut_value())
|
||||
}
|
||||
|
||||
/// Batch delete multiple edges
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `edges` - JavaScript array of [u, v] tuples
|
||||
///
|
||||
/// # Returns
|
||||
/// The final minimum cut value
|
||||
///
|
||||
/// # Example
|
||||
/// ```javascript
|
||||
/// const edges = [[0, 1], [1, 2]];
|
||||
/// const cutValue = mincut.batchDelete(edges);
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "batchDelete")]
|
||||
pub fn batch_delete(&mut self, edges: JsValue) -> Result<f64, JsError> {
|
||||
let edges_vec: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(edges)
|
||||
.map_err(|e| JsError::new(&format!("Failed to parse edges: {}", e)))?;
|
||||
|
||||
for edge in edges_vec {
|
||||
if edge.len() < 2 {
|
||||
return Err(JsError::new("Each edge must be [u, v] or [u, v, weight]"));
|
||||
}
|
||||
|
||||
let u = edge[0] as u64;
|
||||
let v = edge[1] as u64;
|
||||
|
||||
self.inner.delete_edge(u, v).map_err(|e| {
|
||||
JsError::new(&format!("Failed to delete edge [{}, {}]: {}", u, v, e))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(self.inner.min_cut_value())
|
||||
}
|
||||
|
||||
/// Clear all edges from the graph
|
||||
#[wasm_bindgen]
|
||||
pub fn clear(&mut self) {
|
||||
self.inner = DynamicMinCut::new(MinCutConfig::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the WASM module (call once at startup)
|
||||
///
|
||||
/// This sets up panic hooks for better error messages in the browser console.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
/// Get version information
|
||||
#[wasm_bindgen(js_name = "getVersion")]
|
||||
pub fn get_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ThreeLevelHierarchy - Paper Section 3: Expander → Precluster → Cluster
|
||||
// ============================================================================
|
||||
|
||||
/// Statistics for the hierarchy
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct HierarchyStatsJs {
|
||||
num_expanders: usize,
|
||||
num_preclusters: usize,
|
||||
num_clusters: usize,
|
||||
num_vertices: usize,
|
||||
num_edges: usize,
|
||||
global_min_cut: f64,
|
||||
avg_expander_size: f64,
|
||||
}
|
||||
|
||||
/// WASM wrapper for ThreeLevelHierarchy
|
||||
///
|
||||
/// Implements the 3-level decomposition from arXiv:2512.13105:
|
||||
/// - Level 0: Expanders (φ-expander subgraphs)
|
||||
/// - Level 1: Preclusters (groups of expanders)
|
||||
/// - Level 2: Clusters (top-level grouping with mirror cuts)
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmThreeLevelHierarchy {
|
||||
inner: ThreeLevelHierarchy,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmThreeLevelHierarchy {
|
||||
/// Create a new hierarchy with default configuration
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmThreeLevelHierarchy {
|
||||
console_error_panic_hook::set_once();
|
||||
WasmThreeLevelHierarchy {
|
||||
inner: ThreeLevelHierarchy::with_defaults(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create hierarchy with custom expansion parameter φ
|
||||
#[wasm_bindgen(js_name = "withPhi")]
|
||||
pub fn with_phi(phi: f64) -> WasmThreeLevelHierarchy {
|
||||
console_error_panic_hook::set_once();
|
||||
WasmThreeLevelHierarchy {
|
||||
inner: ThreeLevelHierarchy::new(HierarchyConfig {
|
||||
phi,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge into the graph
|
||||
#[wasm_bindgen(js_name = "insertEdge")]
|
||||
pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) {
|
||||
self.inner.insert_edge(u, v, weight);
|
||||
}
|
||||
|
||||
/// Delete an edge from the graph
|
||||
#[wasm_bindgen(js_name = "deleteEdge")]
|
||||
pub fn delete_edge(&mut self, u: u64, v: u64) {
|
||||
self.inner.delete_edge(u, v);
|
||||
}
|
||||
|
||||
/// Build the complete 3-level hierarchy
|
||||
///
|
||||
/// Must be called after inserting edges to compute the decomposition.
|
||||
#[wasm_bindgen]
|
||||
pub fn build(&mut self) {
|
||||
self.inner.build();
|
||||
}
|
||||
|
||||
/// Get hierarchy statistics as JSON
|
||||
#[wasm_bindgen]
|
||||
pub fn stats(&self) -> JsValue {
|
||||
let s = self.inner.stats();
|
||||
let stats = HierarchyStatsJs {
|
||||
num_expanders: s.num_expanders,
|
||||
num_preclusters: s.num_preclusters,
|
||||
num_clusters: s.num_clusters,
|
||||
num_vertices: s.num_vertices,
|
||||
num_edges: s.num_edges,
|
||||
global_min_cut: s.global_min_cut,
|
||||
avg_expander_size: s.avg_expander_size,
|
||||
};
|
||||
serde_wasm_bindgen::to_value(&stats).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Get the global minimum cut estimate
|
||||
#[wasm_bindgen(js_name = "globalMinCut")]
|
||||
pub fn global_min_cut(&self) -> f64 {
|
||||
self.inner.global_min_cut
|
||||
}
|
||||
|
||||
/// Get all vertices as JSON array
|
||||
#[wasm_bindgen]
|
||||
pub fn vertices(&self) -> JsValue {
|
||||
let verts: Vec<u64> = self.inner.vertices();
|
||||
serde_wasm_bindgen::to_value(&verts).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DeterministicLocalKCut - Paper Theorem 4.1: Color-coded DFS
|
||||
// ============================================================================
|
||||
|
||||
/// Local cut result for JS
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LocalCutJs {
|
||||
cut_value: f64,
|
||||
vertices: Vec<u64>,
|
||||
}
|
||||
|
||||
/// WASM wrapper for DeterministicLocalKCut
|
||||
///
|
||||
/// Implements the deterministic local k-cut algorithm from arXiv:2512.13105:
|
||||
/// - Uses 4-color coding (red-blue, green-yellow)
|
||||
/// - Greedy forest packing for edge classification
|
||||
/// - Color-coded DFS for cut enumeration
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmLocalKCut {
|
||||
inner: DeterministicLocalKCut,
|
||||
num_vertices: usize,
|
||||
num_edges: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmLocalKCut {
|
||||
/// Create a new LocalKCut structure
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `lambda_max` - Maximum cut value to consider
|
||||
/// * `volume_bound` - Maximum volume to explore (nu parameter)
|
||||
/// * `beta` - Cut depth parameter (typically 2)
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(lambda_max: u64, volume_bound: usize, beta: usize) -> WasmLocalKCut {
|
||||
console_error_panic_hook::set_once();
|
||||
WasmLocalKCut {
|
||||
inner: DeterministicLocalKCut::new(lambda_max, volume_bound, beta),
|
||||
num_vertices: 0,
|
||||
num_edges: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge
|
||||
#[wasm_bindgen(js_name = "insertEdge")]
|
||||
pub fn insert_edge(&mut self, u: u64, v: u64, weight: f64) {
|
||||
self.inner.insert_edge(u, v, weight);
|
||||
self.num_edges += 1;
|
||||
// Rough vertex count estimate (may overcount)
|
||||
self.num_vertices = self.num_vertices.max((u.max(v) + 1) as usize);
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
#[wasm_bindgen(js_name = "deleteEdge")]
|
||||
pub fn delete_edge(&mut self, u: u64, v: u64) {
|
||||
self.inner.delete_edge(u, v);
|
||||
self.num_edges = self.num_edges.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Query local cuts from a source vertex
|
||||
///
|
||||
/// Returns array of { cut_value, vertices } objects
|
||||
#[wasm_bindgen]
|
||||
pub fn query(&self, source: u64) -> JsValue {
|
||||
let results = self.inner.query(source);
|
||||
let cuts: Vec<LocalCutJs> = results
|
||||
.into_iter()
|
||||
.map(|c| LocalCutJs {
|
||||
cut_value: c.cut_value,
|
||||
vertices: c.vertices.into_iter().collect(),
|
||||
})
|
||||
.collect();
|
||||
serde_wasm_bindgen::to_value(&cuts).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Get number of vertices (approximate)
|
||||
#[wasm_bindgen(js_name = "numVertices")]
|
||||
pub fn num_vertices(&self) -> usize {
|
||||
self.num_vertices
|
||||
}
|
||||
|
||||
/// Get number of edges
|
||||
#[wasm_bindgen(js_name = "numEdges")]
|
||||
pub fn num_edges(&self) -> usize {
|
||||
self.num_edges
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MinCutWrapper - Full API with Connectivity Curve Analysis
|
||||
// ============================================================================
|
||||
|
||||
/// Connectivity curve point
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CurvePoint {
|
||||
k: usize,
|
||||
min_cut: u64,
|
||||
}
|
||||
|
||||
/// Elbow detection result
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ElbowResult {
|
||||
k: usize,
|
||||
drop: u64,
|
||||
}
|
||||
|
||||
/// WASM wrapper for MinCutWrapper
|
||||
///
|
||||
/// High-level API combining all paper algorithms:
|
||||
/// - O(log n) instance management
|
||||
/// - ThreeLevelHierarchy decomposition
|
||||
/// - LocalKCut discovery
|
||||
/// - Connectivity curve analysis for boundary validation
|
||||
#[wasm_bindgen]
|
||||
pub struct WasmMinCutWrapper {
|
||||
inner: MinCutWrapper,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMinCutWrapper {
|
||||
/// Create a new MinCutWrapper
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> WasmMinCutWrapper {
|
||||
console_error_panic_hook::set_once();
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
WasmMinCutWrapper {
|
||||
inner: MinCutWrapper::new(graph),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge (timestamp auto-incremented)
|
||||
#[wasm_bindgen(js_name = "insertEdge")]
|
||||
pub fn insert_edge(&mut self, u: u64, v: u64) {
|
||||
let time = self.inner.current_time() + 1;
|
||||
self.inner.insert_edge(time, u, v);
|
||||
}
|
||||
|
||||
/// Delete an edge (timestamp auto-incremented)
|
||||
#[wasm_bindgen(js_name = "deleteEdge")]
|
||||
pub fn delete_edge(&mut self, u: u64, v: u64) {
|
||||
let time = self.inner.current_time() + 1;
|
||||
self.inner.delete_edge(time, u, v);
|
||||
}
|
||||
|
||||
/// Query the minimum cut value
|
||||
#[wasm_bindgen]
|
||||
pub fn query(&mut self) -> f64 {
|
||||
self.inner.min_cut_value() as f64
|
||||
}
|
||||
|
||||
/// Get number of active instances
|
||||
#[wasm_bindgen(js_name = "numInstances")]
|
||||
pub fn num_instances(&self) -> usize {
|
||||
self.inner.num_instances()
|
||||
}
|
||||
|
||||
/// Get current logical time
|
||||
#[wasm_bindgen(js_name = "currentTime")]
|
||||
pub fn current_time(&self) -> u64 {
|
||||
self.inner.current_time()
|
||||
}
|
||||
|
||||
/// Query with LocalKCut certification
|
||||
///
|
||||
/// Returns { cut_value, certified } object
|
||||
#[wasm_bindgen(js_name = "queryWithCertification")]
|
||||
pub fn query_with_certification(&mut self, source: u64) -> JsValue {
|
||||
let (cut_value, certified) = self.inner.query_with_local_kcut(source);
|
||||
let result = serde_json::json!({
|
||||
"cut_value": cut_value,
|
||||
"certified": certified,
|
||||
});
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Get local cuts from a source vertex
|
||||
#[wasm_bindgen(js_name = "localCuts")]
|
||||
pub fn local_cuts(&self, source: u64, lambda_max: u64) -> JsValue {
|
||||
let cuts = self.inner.local_cuts(source, lambda_max);
|
||||
let result: Vec<LocalCutJs> = cuts
|
||||
.into_iter()
|
||||
.map(|(value, verts)| LocalCutJs {
|
||||
cut_value: value,
|
||||
vertices: verts,
|
||||
})
|
||||
.collect();
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Compute edge-connectivity degradation curve
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ranked_edges` - Array of [u, v, score] ranked by cut-likelihood
|
||||
/// * `k_max` - Maximum edges to remove
|
||||
///
|
||||
/// # Returns
|
||||
/// Array of { k, min_cut } showing degradation
|
||||
#[wasm_bindgen(js_name = "connectivityCurve")]
|
||||
pub fn connectivity_curve(&self, ranked_edges: JsValue, k_max: usize) -> JsValue {
|
||||
let edges: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(ranked_edges).unwrap_or_default();
|
||||
|
||||
let ranked: Vec<(u64, u64, f64)> = edges
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
if e.len() >= 3 {
|
||||
Some((e[0] as u64, e[1] as u64, e[2]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let curve = self.inner.connectivity_curve(&ranked, k_max);
|
||||
let result: Vec<CurvePoint> = curve
|
||||
.into_iter()
|
||||
.map(|(k, min_cut)| CurvePoint { k, min_cut })
|
||||
.collect();
|
||||
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// Find elbow point in connectivity curve
|
||||
///
|
||||
/// Returns { k, drop } or null if no elbow found
|
||||
#[wasm_bindgen(js_name = "findElbow")]
|
||||
pub fn find_elbow(curve: JsValue) -> JsValue {
|
||||
let points: Vec<CurvePoint> = serde_wasm_bindgen::from_value(curve).unwrap_or_default();
|
||||
|
||||
let curve_data: Vec<(usize, u64)> = points.into_iter().map(|p| (p.k, p.min_cut)).collect();
|
||||
|
||||
match MinCutWrapper::find_elbow(&curve_data) {
|
||||
Some((k, drop)) => {
|
||||
let result = ElbowResult { k, drop };
|
||||
serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute detector quality score
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ranked_edges` - Array of [u, v, score]
|
||||
/// * `true_cut_size` - Known size of true minimum cut
|
||||
///
|
||||
/// # Returns
|
||||
/// Quality score from 0.0 (poor) to 1.0 (perfect)
|
||||
#[wasm_bindgen(js_name = "detectorQuality")]
|
||||
pub fn detector_quality(&self, ranked_edges: JsValue, true_cut_size: usize) -> f64 {
|
||||
let edges: Vec<Vec<f64>> = serde_wasm_bindgen::from_value(ranked_edges).unwrap_or_default();
|
||||
|
||||
let ranked: Vec<(u64, u64, f64)> = edges
|
||||
.into_iter()
|
||||
.filter_map(|e| {
|
||||
if e.len() >= 3 {
|
||||
Some((e[0] as u64, e[1] as u64, e[2]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.inner.detector_quality(&ranked, true_cut_size)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_new() {
|
||||
let mincut = WasmMinCut::new().unwrap();
|
||||
assert_eq!(mincut.num_vertices(), 0);
|
||||
assert_eq!(mincut.num_edges(), 0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_insert_edge() {
|
||||
let mut mincut = WasmMinCut::new().unwrap();
|
||||
let result = mincut.insert_edge(0, 1, 1.0);
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(mincut.num_edges(), 1);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_min_cut_value() {
|
||||
let mut mincut = WasmMinCut::new().unwrap();
|
||||
mincut.insert_edge(0, 1, 1.0).unwrap();
|
||||
mincut.insert_edge(1, 2, 2.0).unwrap();
|
||||
mincut.insert_edge(0, 2, 1.5).unwrap();
|
||||
|
||||
let cut_value = mincut.min_cut_value();
|
||||
assert!(cut_value > 0.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user