Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
545
vendor/ruvector/crates/ruvector-mincut-node/src/lib.rs
vendored
Normal file
545
vendor/ruvector/crates/ruvector-mincut-node/src/lib.rs
vendored
Normal file
@@ -0,0 +1,545 @@
|
||||
//! Node.js bindings for RuVector MinCut
|
||||
//!
|
||||
//! Provides native Node.js API for dynamic minimum cut operations,
|
||||
//! including paper algorithms from arXiv:2512.13105.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **MinCut**: Basic dynamic minimum cut (insert/delete/query)
|
||||
//! - **ThreeLevelHierarchy**: 3-level decomposition (Expander→Precluster→Cluster)
|
||||
//! - **LocalKCut**: Deterministic local k-cut discovery with 4-color coding
|
||||
//! - **MinCutWrapper**: Full API with connectivity curve analysis
|
||||
|
||||
use napi::bindgen_prelude::*;
|
||||
use napi_derive::napi;
|
||||
use ruvector_mincut::cluster::hierarchy::{HierarchyConfig, ThreeLevelHierarchy as RustHierarchy};
|
||||
use ruvector_mincut::localkcut::deterministic::DeterministicLocalKCut;
|
||||
use ruvector_mincut::{
|
||||
DynamicGraph, DynamicMinCut, MinCutBuilder, MinCutWrapper as RustMinCutWrapper,
|
||||
};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Edge representation for JavaScript
|
||||
#[napi(object)]
|
||||
pub struct JsEdge {
|
||||
pub id: u32,
|
||||
pub source: u32,
|
||||
pub target: u32,
|
||||
pub weight: f64,
|
||||
}
|
||||
|
||||
/// Statistics about the algorithm
|
||||
#[napi(object)]
|
||||
pub struct JsStats {
|
||||
pub insertions: u32,
|
||||
pub deletions: u32,
|
||||
pub queries: u32,
|
||||
pub avg_update_time_us: f64,
|
||||
}
|
||||
|
||||
/// Minimum cut result
|
||||
#[napi(object)]
|
||||
pub struct JsMinCutResult {
|
||||
pub value: f64,
|
||||
pub is_exact: bool,
|
||||
pub approximation_ratio: f64,
|
||||
}
|
||||
|
||||
/// Configuration for minimum cut
|
||||
#[napi(object)]
|
||||
pub struct JsMinCutConfig {
|
||||
pub approximate: Option<bool>,
|
||||
pub epsilon: Option<f64>,
|
||||
pub max_exact_cut_size: Option<u32>,
|
||||
}
|
||||
|
||||
/// Partition result
|
||||
#[napi(object)]
|
||||
pub struct JsPartition {
|
||||
pub s: Vec<u32>,
|
||||
pub t: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Node.js wrapper for DynamicMinCut
|
||||
#[napi]
|
||||
pub struct MinCut {
|
||||
inner: Arc<Mutex<DynamicMinCut>>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MinCut {
|
||||
/// Create a new empty minimum cut structure
|
||||
#[napi(constructor)]
|
||||
pub fn new(config: Option<JsMinCutConfig>) -> Result<Self> {
|
||||
let mut builder = MinCutBuilder::new();
|
||||
|
||||
if let Some(cfg) = config {
|
||||
if cfg.approximate.unwrap_or(false) {
|
||||
builder = builder.approximate(cfg.epsilon.unwrap_or(0.1));
|
||||
}
|
||||
if let Some(max_size) = cfg.max_exact_cut_size {
|
||||
builder = builder.max_cut_size(max_size as usize);
|
||||
}
|
||||
}
|
||||
|
||||
let mincut = builder
|
||||
.build()
|
||||
.map_err(|e| Error::from_reason(format!("Failed to create MinCut: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(mincut)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from edges array
|
||||
#[napi(factory)]
|
||||
pub fn from_edges(edges: Vec<(u32, u32, f64)>, config: Option<JsMinCutConfig>) -> Result<Self> {
|
||||
let mut builder = MinCutBuilder::new();
|
||||
|
||||
if let Some(cfg) = config {
|
||||
if cfg.approximate.unwrap_or(false) {
|
||||
builder = builder.approximate(cfg.epsilon.unwrap_or(0.1));
|
||||
}
|
||||
if let Some(max_size) = cfg.max_exact_cut_size {
|
||||
builder = builder.max_cut_size(max_size as usize);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert edges to the expected format
|
||||
let edge_tuples: Vec<(u64, u64, f64)> = edges
|
||||
.into_iter()
|
||||
.map(|(u, v, w)| (u as u64, v as u64, w))
|
||||
.collect();
|
||||
|
||||
let mincut = builder.with_edges(edge_tuples).build().map_err(|e| {
|
||||
Error::from_reason(format!("Failed to create MinCut from edges: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(mincut)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert an edge (returns new min cut value)
|
||||
#[napi]
|
||||
pub fn insert_edge(&self, u: u32, v: u32, weight: f64) -> Result<f64> {
|
||||
let mut mincut = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|e| Error::from_reason(format!("Lock error: {}", e)))?;
|
||||
|
||||
mincut
|
||||
.insert_edge(u as u64, v as u64, weight)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to insert edge: {}", e)))
|
||||
}
|
||||
|
||||
/// Delete an edge (returns new min cut value)
|
||||
#[napi]
|
||||
pub fn delete_edge(&self, u: u32, v: u32) -> Result<f64> {
|
||||
let mut mincut = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|e| Error::from_reason(format!("Lock error: {}", e)))?;
|
||||
|
||||
mincut
|
||||
.delete_edge(u as u64, v as u64)
|
||||
.map_err(|e| Error::from_reason(format!("Failed to delete edge: {}", e)))
|
||||
}
|
||||
|
||||
/// Get minimum cut value
|
||||
#[napi(getter)]
|
||||
pub fn min_cut_value(&self) -> f64 {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
mincut.min_cut_value()
|
||||
}
|
||||
|
||||
/// Get detailed minimum cut result
|
||||
#[napi]
|
||||
pub fn min_cut(&self) -> JsMinCutResult {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
let result = mincut.min_cut();
|
||||
|
||||
JsMinCutResult {
|
||||
value: result.value,
|
||||
is_exact: result.is_exact,
|
||||
approximation_ratio: result.approximation_ratio,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get partition: returns { s: number[], t: number[] }
|
||||
#[napi]
|
||||
pub fn partition(&self) -> Result<JsPartition> {
|
||||
let mincut = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|e| Error::from_reason(format!("Lock error: {}", e)))?;
|
||||
|
||||
let (s, t) = mincut.partition();
|
||||
|
||||
Ok(JsPartition {
|
||||
s: s.into_iter().map(|v| v as u32).collect(),
|
||||
t: t.into_iter().map(|v| v as u32).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get cut edges
|
||||
#[napi]
|
||||
pub fn cut_edges(&self) -> Vec<JsEdge> {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
let edges = mincut.cut_edges();
|
||||
|
||||
edges
|
||||
.into_iter()
|
||||
.map(|e| JsEdge {
|
||||
id: e.id as u32,
|
||||
source: e.source as u32,
|
||||
target: e.target as u32,
|
||||
weight: e.weight,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get number of vertices
|
||||
#[napi(getter)]
|
||||
pub fn num_vertices(&self) -> u32 {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
mincut.num_vertices() as u32
|
||||
}
|
||||
|
||||
/// Get number of edges
|
||||
#[napi(getter)]
|
||||
pub fn num_edges(&self) -> u32 {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
mincut.num_edges() as u32
|
||||
}
|
||||
|
||||
/// Check if graph is connected
|
||||
#[napi]
|
||||
pub fn is_connected(&self) -> bool {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
mincut.is_connected()
|
||||
}
|
||||
|
||||
/// Get algorithm statistics
|
||||
#[napi(getter)]
|
||||
pub fn stats(&self) -> JsStats {
|
||||
let mincut = self.inner.lock().unwrap();
|
||||
let stats = mincut.stats();
|
||||
|
||||
JsStats {
|
||||
insertions: stats.insertions as u32,
|
||||
deletions: stats.deletions as u32,
|
||||
queries: stats.queries as u32,
|
||||
avg_update_time_us: stats.avg_update_time_us,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset statistics
|
||||
#[napi]
|
||||
pub fn reset_stats(&self) {
|
||||
let mut mincut = self.inner.lock().unwrap();
|
||||
mincut.reset_stats();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ThreeLevelHierarchy - Paper Section 3: Expander → Precluster → Cluster
|
||||
// ============================================================================
|
||||
|
||||
/// Hierarchy statistics
|
||||
#[napi(object)]
|
||||
pub struct JsHierarchyStats {
|
||||
pub num_expanders: u32,
|
||||
pub num_preclusters: u32,
|
||||
pub num_clusters: u32,
|
||||
pub num_vertices: u32,
|
||||
pub num_edges: u32,
|
||||
pub global_min_cut: f64,
|
||||
pub avg_expander_size: f64,
|
||||
}
|
||||
|
||||
/// Three-level hierarchy decomposition from the paper
|
||||
#[napi]
|
||||
pub struct ThreeLevelHierarchy {
|
||||
inner: RustHierarchy,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl ThreeLevelHierarchy {
|
||||
/// Create with default configuration
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
ThreeLevelHierarchy {
|
||||
inner: RustHierarchy::with_defaults(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with custom expansion parameter φ
|
||||
#[napi(factory)]
|
||||
pub fn with_phi(phi: f64) -> Self {
|
||||
ThreeLevelHierarchy {
|
||||
inner: RustHierarchy::new(HierarchyConfig {
|
||||
phi,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge
|
||||
#[napi]
|
||||
pub fn insert_edge(&mut self, u: u32, v: u32, weight: f64) {
|
||||
self.inner.insert_edge(u as u64, v as u64, weight);
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
#[napi]
|
||||
pub fn delete_edge(&mut self, u: u32, v: u32) {
|
||||
self.inner.delete_edge(u as u64, v as u64);
|
||||
}
|
||||
|
||||
/// Build the 3-level decomposition
|
||||
#[napi]
|
||||
pub fn build(&mut self) {
|
||||
self.inner.build();
|
||||
}
|
||||
|
||||
/// Get hierarchy statistics
|
||||
#[napi(getter)]
|
||||
pub fn stats(&self) -> JsHierarchyStats {
|
||||
let s = self.inner.stats();
|
||||
JsHierarchyStats {
|
||||
num_expanders: s.num_expanders as u32,
|
||||
num_preclusters: s.num_preclusters as u32,
|
||||
num_clusters: s.num_clusters as u32,
|
||||
num_vertices: s.num_vertices as u32,
|
||||
num_edges: s.num_edges as u32,
|
||||
global_min_cut: s.global_min_cut,
|
||||
avg_expander_size: s.avg_expander_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get global minimum cut estimate
|
||||
#[napi(getter)]
|
||||
pub fn global_min_cut(&self) -> f64 {
|
||||
self.inner.global_min_cut
|
||||
}
|
||||
|
||||
/// Get all vertices
|
||||
#[napi]
|
||||
pub fn vertices(&self) -> Vec<u32> {
|
||||
self.inner
|
||||
.vertices()
|
||||
.into_iter()
|
||||
.map(|v| v as u32)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LocalKCut - Paper Theorem 4.1: Color-coded DFS
|
||||
// ============================================================================
|
||||
|
||||
/// Local cut result
|
||||
#[napi(object)]
|
||||
pub struct JsLocalCut {
|
||||
pub cut_value: f64,
|
||||
pub vertices: Vec<u32>,
|
||||
}
|
||||
|
||||
/// Deterministic local k-cut algorithm
|
||||
#[napi]
|
||||
pub struct LocalKCut {
|
||||
inner: DeterministicLocalKCut,
|
||||
num_vertices: usize,
|
||||
num_edges: usize,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl LocalKCut {
|
||||
/// Create new LocalKCut structure
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `lambda_max` - Maximum cut value
|
||||
/// * `volume_bound` - Maximum volume (nu parameter)
|
||||
/// * `beta` - Cut depth parameter
|
||||
#[napi(constructor)]
|
||||
pub fn new(lambda_max: i64, volume_bound: u32, beta: u32) -> Self {
|
||||
LocalKCut {
|
||||
inner: DeterministicLocalKCut::new(
|
||||
lambda_max as u64,
|
||||
volume_bound as usize,
|
||||
beta as usize,
|
||||
),
|
||||
num_vertices: 0,
|
||||
num_edges: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge
|
||||
#[napi]
|
||||
pub fn insert_edge(&mut self, u: u32, v: u32, weight: f64) {
|
||||
self.inner.insert_edge(u as u64, v as u64, weight);
|
||||
self.num_edges += 1;
|
||||
self.num_vertices = self.num_vertices.max((u.max(v) + 1) as usize);
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
#[napi]
|
||||
pub fn delete_edge(&mut self, u: u32, v: u32) {
|
||||
self.inner.delete_edge(u as u64, v as u64);
|
||||
self.num_edges = self.num_edges.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Query local cuts from a source
|
||||
#[napi]
|
||||
pub fn query(&self, source: u32) -> Vec<JsLocalCut> {
|
||||
self.inner
|
||||
.query(source as u64)
|
||||
.into_iter()
|
||||
.map(|c| JsLocalCut {
|
||||
cut_value: c.cut_value,
|
||||
vertices: c.vertices.into_iter().map(|v| v as u32).collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get number of vertices
|
||||
#[napi(getter)]
|
||||
pub fn num_vertices(&self) -> u32 {
|
||||
self.num_vertices as u32
|
||||
}
|
||||
|
||||
/// Get number of edges
|
||||
#[napi(getter)]
|
||||
pub fn num_edges(&self) -> u32 {
|
||||
self.num_edges as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MinCutWrapper - Full API with Connectivity Curve Analysis
|
||||
// ============================================================================
|
||||
|
||||
/// Connectivity curve point
|
||||
#[napi(object)]
|
||||
pub struct JsCurvePoint {
|
||||
pub k: u32,
|
||||
pub min_cut: i64,
|
||||
}
|
||||
|
||||
/// Elbow detection result
|
||||
#[napi(object)]
|
||||
pub struct JsElbowResult {
|
||||
pub k: u32,
|
||||
pub drop: i64,
|
||||
}
|
||||
|
||||
/// Full MinCutWrapper with paper algorithms
|
||||
#[napi]
|
||||
pub struct MinCutWrapperNode {
|
||||
inner: RustMinCutWrapper,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MinCutWrapperNode {
|
||||
/// Create new wrapper
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
let graph = Arc::new(DynamicGraph::new());
|
||||
MinCutWrapperNode {
|
||||
inner: RustMinCutWrapper::new(graph),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an edge
|
||||
#[napi]
|
||||
pub fn insert_edge(&mut self, u: u32, v: u32) {
|
||||
let time = self.inner.current_time() + 1;
|
||||
self.inner.insert_edge(time, u as u64, v as u64);
|
||||
}
|
||||
|
||||
/// Delete an edge
|
||||
#[napi]
|
||||
pub fn delete_edge(&mut self, u: u32, v: u32) {
|
||||
let time = self.inner.current_time() + 1;
|
||||
self.inner.delete_edge(time, u as u64, v as u64);
|
||||
}
|
||||
|
||||
/// Query minimum cut
|
||||
#[napi]
|
||||
pub fn query(&mut self) -> i64 {
|
||||
self.inner.min_cut_value() as i64
|
||||
}
|
||||
|
||||
/// Get number of instances
|
||||
#[napi(getter)]
|
||||
pub fn num_instances(&self) -> u32 {
|
||||
self.inner.num_instances() as u32
|
||||
}
|
||||
|
||||
/// Get current time
|
||||
#[napi(getter)]
|
||||
pub fn current_time(&self) -> i64 {
|
||||
self.inner.current_time() as i64
|
||||
}
|
||||
|
||||
/// Get local cuts from source
|
||||
#[napi]
|
||||
pub fn local_cuts(&self, source: u32, lambda_max: i64) -> Vec<JsLocalCut> {
|
||||
self.inner
|
||||
.local_cuts(source as u64, lambda_max as u64)
|
||||
.into_iter()
|
||||
.map(|(value, verts)| JsLocalCut {
|
||||
cut_value: value,
|
||||
vertices: verts.into_iter().map(|v| v as u32).collect(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compute connectivity curve
|
||||
#[napi]
|
||||
pub fn connectivity_curve(
|
||||
&self,
|
||||
ranked_edges: Vec<(u32, u32, f64)>,
|
||||
k_max: u32,
|
||||
) -> Vec<JsCurvePoint> {
|
||||
let ranked: Vec<(u64, u64, f64)> = ranked_edges
|
||||
.into_iter()
|
||||
.map(|(u, v, s)| (u as u64, v as u64, s))
|
||||
.collect();
|
||||
|
||||
self.inner
|
||||
.connectivity_curve(&ranked, k_max as usize)
|
||||
.into_iter()
|
||||
.map(|(k, min_cut)| JsCurvePoint {
|
||||
k: k as u32,
|
||||
min_cut: min_cut as i64,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find elbow in curve
|
||||
#[napi]
|
||||
pub fn find_elbow(curve: Vec<JsCurvePoint>) -> Option<JsElbowResult> {
|
||||
let curve_data: Vec<(usize, u64)> = curve
|
||||
.into_iter()
|
||||
.map(|p| (p.k as usize, p.min_cut as u64))
|
||||
.collect();
|
||||
|
||||
RustMinCutWrapper::find_elbow(&curve_data).map(|(k, drop)| JsElbowResult {
|
||||
k: k as u32,
|
||||
drop: drop as i64,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute detector quality
|
||||
#[napi]
|
||||
pub fn detector_quality(&self, ranked_edges: Vec<(u32, u32, f64)>, true_cut_size: u32) -> f64 {
|
||||
let ranked: Vec<(u64, u64, f64)> = ranked_edges
|
||||
.into_iter()
|
||||
.map(|(u, v, s)| (u as u64, v as u64, s))
|
||||
.collect();
|
||||
|
||||
self.inner.detector_quality(&ranked, true_cut_size as usize)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user