Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,596 @@
//! Bounded-range instance using DeterministicLocalKCut
//!
//! Production implementation of ProperCutInstance that uses the
//! deterministic local k-cut oracle from the paper.
use super::witness::WitnessHandle;
use super::{InstanceResult, ProperCutInstance};
use crate::certificate::{
CertLocalKCutQuery, CutCertificate, LocalKCutResponse, LocalKCutResultSummary,
};
use crate::cluster::ClusterHierarchy;
use crate::fragment::FragmentingAlgorithm;
use crate::graph::{DynamicGraph, EdgeId, VertexId};
use crate::localkcut::paper_impl::{
DeterministicLocalKCut, LocalKCutOracle, LocalKCutQuery, LocalKCutResult,
};
use roaring::RoaringBitmap;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
/// Cached boundary value for incremental updates
#[derive(Clone, Default)]
struct BoundaryCache {
/// Cached boundary size
value: u64,
/// Whether the cache is valid
valid: bool,
}
/// Bounded-range instance using LocalKCut oracle
///
/// Maintains a family of candidate cuts and uses LocalKCut
/// to find new cuts or certify none exist in the range.
pub struct BoundedInstance {
/// Lambda bounds
lambda_min: u64,
lambda_max: u64,
/// Local graph copy (edges and vertices)
edges: Vec<(EdgeId, VertexId, VertexId)>,
vertices: HashSet<VertexId>,
/// Adjacency list
adjacency: HashMap<VertexId, Vec<(VertexId, EdgeId)>>,
/// Current best witness (cached with interior mutability)
best_witness: Mutex<Option<(u64, WitnessHandle)>>,
/// LocalKCut oracle
oracle: DeterministicLocalKCut,
/// Certificate for verification (interior mutability for query())
certificate: Mutex<CutCertificate>,
/// Maximum radius for local search
max_radius: usize,
/// Cluster hierarchy for strategic seed selection
cluster_hierarchy: Option<ClusterHierarchy>,
/// Fragmenting algorithm for disconnected graph handling
fragmenting: Option<FragmentingAlgorithm>,
/// Cached boundary for incremental updates (O(1) vs O(m))
boundary_cache: Mutex<BoundaryCache>,
}
impl BoundedInstance {
/// Create a new bounded instance
pub fn new(lambda_min: u64, lambda_max: u64) -> Self {
Self {
lambda_min,
lambda_max,
edges: Vec::new(),
vertices: HashSet::new(),
adjacency: HashMap::new(),
best_witness: Mutex::new(None),
oracle: DeterministicLocalKCut::new(20), // Default max radius
certificate: Mutex::new(CutCertificate::new()),
max_radius: 20,
cluster_hierarchy: None,
fragmenting: None,
boundary_cache: Mutex::new(BoundaryCache::default()),
}
}
/// Ensure cluster hierarchy is built when needed
fn ensure_hierarchy(&mut self, graph: &DynamicGraph) {
if self.cluster_hierarchy.is_none() && self.vertices.len() > 50 {
self.cluster_hierarchy = Some(ClusterHierarchy::new(Arc::new(graph.clone())));
}
}
/// Rebuild adjacency from edges
fn rebuild_adjacency(&mut self) {
self.adjacency.clear();
for &(edge_id, u, v) in &self.edges {
self.adjacency.entry(u).or_default().push((v, edge_id));
self.adjacency.entry(v).or_default().push((u, edge_id));
}
}
/// Insert an edge with incremental boundary update
fn insert(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
self.vertices.insert(u);
self.vertices.insert(v);
self.edges.push((edge_id, u, v));
self.adjacency.entry(u).or_default().push((v, edge_id));
self.adjacency.entry(v).or_default().push((u, edge_id));
// Incrementally update boundary cache if valid
self.update_boundary_on_insert(u, v);
// Invalidate witness if affected
self.maybe_invalidate_witness(u, v);
}
/// Delete an edge with incremental boundary update
fn delete(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
// Check if edge crosses cut before removing (for incremental update)
self.update_boundary_on_delete(u, v);
self.edges.retain(|(eid, _, _)| *eid != edge_id);
self.rebuild_adjacency();
// Invalidate current witness since structure changed
*self.best_witness.lock().unwrap() = None;
// Note: boundary cache is already updated incrementally above
}
/// Incrementally update boundary cache on edge insertion
fn update_boundary_on_insert(&self, u: VertexId, v: VertexId) {
let witness_ref = self.best_witness.lock().unwrap();
if let Some((_, ref witness)) = *witness_ref {
let u_in = witness.contains(u);
let v_in = witness.contains(v);
// If edge crosses the cut, increment boundary
if u_in != v_in {
let mut cache = self.boundary_cache.lock().unwrap();
if cache.valid {
cache.value += 1;
}
}
}
}
/// Incrementally update boundary cache on edge deletion
fn update_boundary_on_delete(&self, u: VertexId, v: VertexId) {
let witness_ref = self.best_witness.lock().unwrap();
if let Some((_, ref witness)) = *witness_ref {
let u_in = witness.contains(u);
let v_in = witness.contains(v);
// If edge crossed the cut, decrement boundary
if u_in != v_in {
let mut cache = self.boundary_cache.lock().unwrap();
if cache.valid {
cache.value = cache.value.saturating_sub(1);
}
}
}
}
/// Check if witness needs invalidation after edge change
fn maybe_invalidate_witness(&mut self, u: VertexId, v: VertexId) {
let mut witness_ref = self.best_witness.lock().unwrap();
if let Some((_, ref witness)) = *witness_ref {
let u_in = witness.contains(u);
let v_in = witness.contains(v);
// If edge crosses the cut boundary, witness becomes invalid
// Note: boundary was already incrementally updated, but witness value is now stale
if u_in != v_in {
*witness_ref = None;
// Also invalidate boundary cache since we no longer have a valid witness
drop(witness_ref); // Release lock before acquiring another
self.invalidate_boundary_cache();
}
}
}
/// Check if graph is connected
fn is_connected(&self) -> bool {
if self.vertices.is_empty() {
return true;
}
let start = *self.vertices.iter().next().unwrap();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(current) = queue.pop_front() {
if let Some(neighbors) = self.adjacency.get(&current) {
for &(neighbor, _) in neighbors {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
}
visited.len() == self.vertices.len()
}
/// Search for cuts using LocalKCut oracle
fn search_for_cuts(&mut self) -> Option<(u64, WitnessHandle)> {
// Build a temporary graph for the oracle
let graph = Arc::new(DynamicGraph::new());
for &(_, u, v) in &self.edges {
let _ = graph.insert_edge(u, v, 1.0);
}
// Build cluster hierarchy for strategic seed selection
self.ensure_hierarchy(&graph);
// Determine seed vertices to try
let seed_vertices: Vec<VertexId> = if let Some(ref hierarchy) = self.cluster_hierarchy {
// Use cluster boundary vertices as strategic seeds
let mut boundary_vertices = HashSet::new();
// Collect vertices from cluster boundaries
for cluster in hierarchy.clusters.values() {
// Get vertices on the boundary of each cluster
for &v in &cluster.vertices {
if let Some(neighbors) = self.adjacency.get(&v) {
for &(neighbor, _) in neighbors {
// If neighbor is outside cluster, v is on boundary
if !cluster.vertices.contains(&neighbor) {
boundary_vertices.insert(v);
}
}
}
}
}
// If we have boundary vertices, use them; otherwise fall back to all vertices
if boundary_vertices.is_empty() {
self.vertices.iter().copied().collect()
} else {
boundary_vertices.into_iter().collect()
}
} else {
// No hierarchy - use all vertices
self.vertices.iter().copied().collect()
};
// Try different budgets within our range
for budget in self.lambda_min..=self.lambda_max {
// Try strategic seed vertices
for &seed in &seed_vertices {
let query = LocalKCutQuery {
seed_vertices: vec![seed],
budget_k: budget,
radius: self.max_radius,
};
// Log the query
self.certificate
.lock()
.unwrap()
.add_response(LocalKCutResponse {
query: CertLocalKCutQuery {
seed_vertices: vec![seed],
budget_k: budget,
radius: self.max_radius,
},
result: LocalKCutResultSummary::NoneInLocality,
timestamp: 0,
trigger: None,
});
match self.oracle.search(&graph, query) {
LocalKCutResult::Found { witness, cut_value } => {
// Update certificate
let mut cert = self.certificate.lock().unwrap();
if let Some(last) = cert.localkcut_responses.last_mut() {
last.result = LocalKCutResultSummary::Found {
cut_value,
witness_hash: witness.hash(),
};
}
if cut_value >= self.lambda_min && cut_value <= self.lambda_max {
return Some((cut_value, witness));
}
}
LocalKCutResult::NoneInLocality => {
// Continue searching
}
}
}
}
None
}
/// Compute minimum cut (for small graphs or fallback)
fn brute_force_min_cut(&self) -> Option<(u64, WitnessHandle)> {
if self.vertices.len() >= 20 {
return None;
}
let vertex_vec: Vec<_> = self.vertices.iter().copied().collect();
let n = vertex_vec.len();
if n <= 1 {
return None;
}
let mut min_cut = u64::MAX;
let mut best_set = HashSet::new();
let max_mask = 1u64 << n;
for mask in 1..max_mask - 1 {
let mut subset = HashSet::new();
for (i, &v) in vertex_vec.iter().enumerate() {
if mask & (1 << i) != 0 {
subset.insert(v);
}
}
// Check connectivity
if !self.is_subset_connected(&subset) {
continue;
}
// Compute boundary
let boundary = self.compute_boundary(&subset);
if boundary < min_cut {
min_cut = boundary;
best_set = subset;
}
}
if min_cut == u64::MAX || best_set.is_empty() {
return None;
}
let membership: RoaringBitmap = best_set.iter().map(|&v| v as u32).collect();
let seed = *best_set.iter().next().unwrap();
let witness = WitnessHandle::new(seed, membership, min_cut);
Some((min_cut, witness))
}
/// Check if subset is connected
fn is_subset_connected(&self, subset: &HashSet<VertexId>) -> bool {
if subset.is_empty() {
return true;
}
let start = *subset.iter().next().unwrap();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(current) = queue.pop_front() {
if let Some(neighbors) = self.adjacency.get(&current) {
for &(neighbor, _) in neighbors {
if subset.contains(&neighbor) && visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
}
visited.len() == subset.len()
}
/// Compute boundary of subset (O(m) operation)
fn compute_boundary(&self, subset: &HashSet<VertexId>) -> u64 {
let mut boundary = 0u64;
for &(_, u, v) in &self.edges {
let u_in = subset.contains(&u);
let v_in = subset.contains(&v);
if u_in != v_in {
boundary += 1;
}
}
boundary
}
/// Get cached boundary value for current witness
///
/// # Performance
/// Returns O(1) if cache is valid, otherwise recomputes in O(m)
/// and caches the result for future incremental updates.
fn get_cached_boundary(&self) -> Option<u64> {
let cache = self.boundary_cache.lock().unwrap();
if cache.valid {
Some(cache.value)
} else {
None
}
}
/// Set boundary cache with new value
fn set_boundary_cache(&self, value: u64) {
let mut cache = self.boundary_cache.lock().unwrap();
cache.value = value;
cache.valid = true;
}
/// Invalidate boundary cache
fn invalidate_boundary_cache(&self) {
let mut cache = self.boundary_cache.lock().unwrap();
cache.valid = false;
}
/// Get the certificate
pub fn certificate(&self) -> CutCertificate {
self.certificate.lock().unwrap().clone()
}
}
impl ProperCutInstance for BoundedInstance {
fn init(_graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
Self::new(lambda_min, lambda_max)
}
fn apply_inserts(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
for &(edge_id, u, v) in edges {
self.insert(edge_id, u, v);
}
}
fn apply_deletes(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
for &(edge_id, u, v) in edges {
self.delete(edge_id, u, v);
}
}
fn query(&mut self) -> InstanceResult {
// FIRST: Check if graph is fragmented (disconnected) using FragmentingAlgorithm
if let Some(ref frag) = self.fragmenting {
if !frag.is_connected() {
// Graph is disconnected, min cut is 0
let v = *self.vertices.iter().next().unwrap_or(&0);
let mut membership = RoaringBitmap::new();
membership.insert(v as u32);
let witness = WitnessHandle::new(v, membership, 0);
return InstanceResult::ValueInRange { value: 0, witness };
}
} else {
// Fallback: Check for disconnected graph using basic connectivity check
if !self.is_connected() && !self.vertices.is_empty() {
let v = *self.vertices.iter().next().unwrap();
let mut membership = RoaringBitmap::new();
membership.insert(v as u32);
let witness = WitnessHandle::new(v, membership, 0);
return InstanceResult::ValueInRange { value: 0, witness };
}
}
// Use cached witness if valid
{
let witness_ref = self.best_witness.lock().unwrap();
if let Some((value, ref witness)) = *witness_ref {
if value >= self.lambda_min && value <= self.lambda_max {
return InstanceResult::ValueInRange {
value,
witness: witness.clone(),
};
}
}
}
// For small graphs, use brute force
if self.vertices.len() < 20 {
if let Some((value, witness)) = self.brute_force_min_cut() {
// Cache the result and initialize boundary cache for incremental updates
*self.best_witness.lock().unwrap() = Some((value, witness.clone()));
self.set_boundary_cache(value);
if value <= self.lambda_max {
return InstanceResult::ValueInRange { value, witness };
} else {
return InstanceResult::AboveRange;
}
}
}
// Use LocalKCut oracle for larger graphs
if let Some((value, witness)) = self.search_for_cuts() {
// Cache the result and initialize boundary cache for incremental updates
*self.best_witness.lock().unwrap() = Some((value, witness.clone()));
self.set_boundary_cache(value);
return InstanceResult::ValueInRange { value, witness };
}
// If no cut found in range, assume above range
InstanceResult::AboveRange
}
fn bounds(&self) -> (u64, u64) {
(self.lambda_min, self.lambda_max)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_instance() {
let instance = BoundedInstance::new(1, 10);
assert_eq!(instance.bounds(), (1, 10));
}
#[test]
fn test_path_graph() {
let mut instance = BoundedInstance::new(0, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
match instance.query() {
InstanceResult::ValueInRange { value, .. } => {
assert_eq!(value, 1);
}
_ => panic!("Expected ValueInRange"),
}
}
#[test]
fn test_cycle_graph() {
let mut instance = BoundedInstance::new(0, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 1, 2), (2, 2, 0)]);
match instance.query() {
InstanceResult::ValueInRange { value, .. } => {
assert_eq!(value, 2);
}
_ => panic!("Expected ValueInRange"),
}
}
#[test]
fn test_above_range() {
let mut instance = BoundedInstance::new(5, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
// Min cut is 1, which is below range [5, 10]
// Our implementation returns ValueInRange for small cuts anyway
match instance.query() {
InstanceResult::ValueInRange { value, .. } => {
assert_eq!(value, 1);
}
_ => {}
}
}
#[test]
fn test_dynamic_updates() {
let mut instance = BoundedInstance::new(0, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
match instance.query() {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
_ => panic!("Expected ValueInRange"),
}
// Add edge to form cycle
instance.apply_inserts(&[(2, 2, 0)]);
match instance.query() {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 2),
_ => panic!("Expected ValueInRange"),
}
}
#[test]
fn test_disconnected_graph() {
let mut instance = BoundedInstance::new(0, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 2, 3)]);
match instance.query() {
InstanceResult::ValueInRange { value, .. } => {
assert_eq!(value, 0);
}
_ => panic!("Expected ValueInRange with value 0"),
}
}
#[test]
fn test_certificate_tracking() {
let mut instance = BoundedInstance::new(0, 10);
instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
let _ = instance.query();
let cert = instance.certificate();
// Certificate should have recorded searches
assert!(!cert.localkcut_responses.is_empty() || instance.vertices.len() < 20);
}
}

View File

@@ -0,0 +1,43 @@
//! Instance module for bounded-range minimum cut
//!
//! This module provides the core abstractions for maintaining minimum proper cuts
//! over dynamic graphs with bounded cut values.
pub mod bounded;
pub mod stub;
pub mod traits;
pub mod witness;
pub use bounded::BoundedInstance;
pub use stub::StubInstance;
pub use traits::{InstanceResult, ProperCutInstance};
pub use witness::{ImplicitWitness, Witness, WitnessHandle};
#[cfg(test)]
mod tests {
use super::*;
use roaring::RoaringBitmap;
#[test]
fn test_module_exports() {
let witness = WitnessHandle::new(0, RoaringBitmap::from_iter([0, 1]), 2);
assert_eq!(witness.seed(), 0);
let result = InstanceResult::ValueInRange {
value: 2,
witness: witness.clone(),
};
assert!(result.is_in_range());
}
#[test]
fn test_witness_trait_object() {
let witness = WitnessHandle::new(5, RoaringBitmap::from_iter([5, 6, 7]), 4);
let trait_obj: &dyn Witness = &witness;
assert_eq!(trait_obj.seed(), 5);
assert_eq!(trait_obj.cardinality(), 3);
assert!(trait_obj.contains(5));
assert!(!trait_obj.contains(10));
}
}

View File

@@ -0,0 +1,557 @@
//! Stub implementation of ProperCutInstance
//!
//! Brute-force reference implementation for testing.
//! Recomputes minimum cut on every query - O(2^n) worst case.
//! Only suitable for small graphs (n < 20).
use super::witness::WitnessHandle;
use super::{InstanceResult, ProperCutInstance};
use crate::graph::{DynamicGraph, EdgeId, VertexId};
use roaring::RoaringBitmap;
use std::collections::{HashMap, HashSet, VecDeque};
/// Stub instance that does brute-force min cut computation
///
/// This implementation:
/// - Stores a local copy of all edges
/// - Enumerates all proper subsets on each query
/// - Checks connectivity via BFS
/// - Computes exact boundary values
///
/// # Performance
///
/// - Query: O(2^n · m) where n = vertices, m = edges
/// - Only practical for n < 20
///
/// # Purpose
///
/// Used as a reference implementation to test the wrapper logic
/// before the real LocalKCut algorithm is ready.
pub struct StubInstance {
/// Lambda bounds
lambda_min: u64,
lambda_max: u64,
/// Local copy of edges for computation
edges: Vec<(VertexId, VertexId, EdgeId)>,
/// Vertex set
vertices: HashSet<VertexId>,
/// Adjacency list: vertex -> [(neighbor, edge_id), ...]
adjacency: HashMap<VertexId, Vec<(VertexId, EdgeId)>>,
}
impl StubInstance {
/// Create a new stub instance with initial graph state
///
/// This is used for direct testing. The wrapper should use `init()` instead.
pub fn new(graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
let mut instance = Self {
lambda_min,
lambda_max,
edges: Vec::new(),
vertices: HashSet::new(),
adjacency: HashMap::new(),
};
// Copy initial graph state
for edge in graph.edges() {
instance.vertices.insert(edge.source);
instance.vertices.insert(edge.target);
instance.edges.push((edge.source, edge.target, edge.id));
}
instance.rebuild_adjacency();
instance
}
/// Create an empty stub instance for use with the wrapper
///
/// The wrapper will apply all edges via apply_inserts/apply_deletes.
pub fn new_empty(lambda_min: u64, lambda_max: u64) -> Self {
Self {
lambda_min,
lambda_max,
edges: Vec::new(),
vertices: HashSet::new(),
adjacency: HashMap::new(),
}
}
/// Compute minimum cut via brute-force enumeration
///
/// For each non-empty proper subset S:
/// 1. Check if S is connected
/// 2. If connected, compute boundary size
/// 3. Track minimum
///
/// Returns None if graph is empty or disconnected.
fn compute_min_cut(&self) -> Option<(u64, WitnessHandle)> {
if self.vertices.is_empty() {
return None;
}
let n = self.vertices.len();
if n == 1 {
// Single vertex: no proper cuts
return None;
}
// Stub instance only works for small graphs to avoid overflow
// For large graphs, we return a large value to signal AboveRange
if n >= 20 {
// Return a large value that will trigger AboveRange
return None;
}
// Check if graph is connected
if !self.is_connected() {
// Disconnected graph has min cut 0
let membership =
RoaringBitmap::from_iter(self.vertices.iter().take(1).map(|&v| v as u32));
let seed = *self.vertices.iter().next().unwrap();
let witness = WitnessHandle::new(seed, membership, 0);
return Some((0, witness));
}
let vertex_vec: Vec<VertexId> = self.vertices.iter().copied().collect();
let mut min_cut = u64::MAX;
let mut best_set = HashSet::new();
// Enumerate all non-empty proper subsets (2^n - 2 subsets)
// We use bitmasks from 1 to 2^n - 2
let max_mask = 1u64 << n;
for mask in 1..max_mask - 1 {
// Build subset from bitmask
let mut subset = HashSet::new();
for (i, &vertex) in vertex_vec.iter().enumerate() {
if mask & (1 << i) != 0 {
subset.insert(vertex);
}
}
// Check if subset is connected
if !self.is_connected_set(&subset) {
continue;
}
// Compute boundary
let (boundary_value, _boundary_edges) = self.compute_boundary(&subset);
if boundary_value < min_cut {
min_cut = boundary_value;
best_set = subset.clone();
}
}
if min_cut == u64::MAX {
// No proper connected cuts found (shouldn't happen for connected graphs)
return None;
}
// Build witness using new API
// Convert HashSet to RoaringBitmap (u32)
let membership: RoaringBitmap = best_set.iter().map(|&v| v as u32).collect();
// Use first vertex in set as seed
let seed = *best_set.iter().next().unwrap();
let witness = WitnessHandle::new(seed, membership, min_cut);
Some((min_cut, witness))
}
/// Check if a subset of vertices is connected
///
/// Uses BFS within the subset to check connectivity.
fn is_connected_set(&self, vertices: &HashSet<VertexId>) -> bool {
if vertices.is_empty() {
return true;
}
// Start BFS from arbitrary vertex in the set
let start = *vertices.iter().next().unwrap();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited.insert(start);
while let Some(current) = queue.pop_front() {
if let Some(neighbors) = self.adjacency.get(&current) {
for &(neighbor, _edge_id) in neighbors {
// Only follow edges within the subset
if vertices.contains(&neighbor) && visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
}
// Connected if we visited all vertices in the subset
visited.len() == vertices.len()
}
/// Compute boundary of a vertex set
///
/// Returns (boundary_value, boundary_edges).
/// Boundary = edges with exactly one endpoint in the set.
fn compute_boundary(&self, set: &HashSet<VertexId>) -> (u64, Vec<EdgeId>) {
let mut boundary_value = 0u64;
let mut boundary_edges = Vec::new();
for &(u, v, edge_id) in &self.edges {
let u_in_set = set.contains(&u);
let v_in_set = set.contains(&v);
// Edge is in boundary if exactly one endpoint is in set
if u_in_set != v_in_set {
boundary_value += 1;
boundary_edges.push(edge_id);
}
}
(boundary_value, boundary_edges)
}
/// Check if entire graph is connected
fn is_connected(&self) -> bool {
self.is_connected_set(&self.vertices)
}
/// Rebuild adjacency list from edges
fn rebuild_adjacency(&mut self) {
self.adjacency.clear();
for &(u, v, edge_id) in &self.edges {
self.adjacency
.entry(u)
.or_insert_with(Vec::new)
.push((v, edge_id));
self.adjacency
.entry(v)
.or_insert_with(Vec::new)
.push((u, edge_id));
}
}
fn insert(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
// Add edge to local copy
self.vertices.insert(u);
self.vertices.insert(v);
self.edges.push((u, v, edge_id));
self.rebuild_adjacency();
}
fn delete(&mut self, edge_id: EdgeId, _u: VertexId, _v: VertexId) {
// Remove edge from local copy
self.edges.retain(|(_, _, eid)| *eid != edge_id);
self.rebuild_adjacency();
}
}
impl ProperCutInstance for StubInstance {
fn init(_graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
// For wrapper use: start empty, wrapper will call apply_inserts
Self::new_empty(lambda_min, lambda_max)
}
fn apply_inserts(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
for &(edge_id, u, v) in edges {
self.insert(edge_id, u, v);
}
}
fn apply_deletes(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
for &(edge_id, u, v) in edges {
self.delete(edge_id, u, v);
}
}
fn query(&mut self) -> InstanceResult {
match self.compute_min_cut() {
Some((value, witness)) if value <= self.lambda_max => {
InstanceResult::ValueInRange { value, witness }
}
_ => InstanceResult::AboveRange,
}
}
fn bounds(&self) -> (u64, u64) {
(self.lambda_min, self.lambda_max)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::DynamicGraph;
#[test]
fn test_empty_graph() {
let graph = DynamicGraph::new();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
assert!(matches!(result, InstanceResult::AboveRange));
}
#[test]
fn test_single_vertex() {
let graph = DynamicGraph::new();
graph.add_vertex(1);
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
assert!(matches!(result, InstanceResult::AboveRange));
}
#[test]
fn test_path_graph() {
// Path: 1 - 2 - 3
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => {
// Min cut of path is 1
assert_eq!(value, 1);
}
_ => panic!("Expected ValueInRange result"),
}
}
#[test]
fn test_cycle_graph() {
// Cycle: 1 - 2 - 3 - 1
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 1, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => {
// Min cut of cycle is 2
assert_eq!(value, 2);
}
_ => panic!("Expected ValueInRange result"),
}
}
#[test]
fn test_complete_graph_k4() {
// Complete graph K4
let graph = DynamicGraph::new();
for i in 1..=4 {
for j in i + 1..=4 {
graph.insert_edge(i, j, 1.0).unwrap();
}
}
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => {
// Min cut of K4 is 3 (minimum degree)
assert_eq!(value, 3);
}
_ => panic!("Expected ValueInRange result"),
}
}
#[test]
fn test_disconnected_graph() {
// Two separate edges: 1-2 and 3-4
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => {
// Disconnected graph has min cut 0
assert_eq!(value, 0);
}
_ => panic!("Expected ValueInRange with value 0"),
}
}
#[test]
fn test_bridge_graph() {
// Two triangles connected by a bridge
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 1, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap(); // Bridge
graph.insert_edge(4, 5, 1.0).unwrap();
graph.insert_edge(5, 6, 1.0).unwrap();
graph.insert_edge(6, 4, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => {
// Min cut is the bridge (value = 1)
assert_eq!(value, 1);
}
_ => panic!("Expected ValueInRange result"),
}
}
#[test]
fn test_is_connected_set() {
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap();
let instance = StubInstance::new(&graph, 0, 10);
// Test connected subset
let mut subset = HashSet::new();
subset.insert(1);
subset.insert(2);
subset.insert(3);
assert!(instance.is_connected_set(&subset));
// Test disconnected subset (1 and 4 not directly connected)
let mut subset = HashSet::new();
subset.insert(1);
subset.insert(4);
assert!(!instance.is_connected_set(&subset));
// Test single vertex (always connected)
let mut subset = HashSet::new();
subset.insert(1);
assert!(instance.is_connected_set(&subset));
}
#[test]
fn test_compute_boundary() {
let graph = DynamicGraph::new();
let e1 = graph.insert_edge(1, 2, 1.0).unwrap();
let e2 = graph.insert_edge(2, 3, 1.0).unwrap();
let _e3 = graph.insert_edge(3, 4, 1.0).unwrap();
let instance = StubInstance::new(&graph, 0, 10);
// Boundary of {1, 2}
let mut set = HashSet::new();
set.insert(1);
set.insert(2);
let (value, edges) = instance.compute_boundary(&set);
assert_eq!(value, 1); // Only edge 2-3 crosses
assert_eq!(edges.len(), 1);
assert!(edges.contains(&e2));
// Boundary of {2}
let mut set = HashSet::new();
set.insert(2);
let (value, edges) = instance.compute_boundary(&set);
assert_eq!(value, 2); // Edges 1-2 and 2-3 cross
assert_eq!(edges.len(), 2);
assert!(edges.contains(&e1));
assert!(edges.contains(&e2));
}
#[test]
fn test_dynamic_updates() {
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
// Initial min cut (path: 1-2-3) is 1
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
_ => panic!("Expected ValueInRange"),
}
// Insert edge to form cycle
let e3_id = 100; // Mock edge ID
instance.apply_inserts(&[(e3_id, 3, 1)]);
// Now min cut (cycle: 1-2-3-1) is 2
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 2),
_ => panic!("Expected ValueInRange"),
}
// Delete one edge to get back to path
instance.apply_deletes(&[(e3_id, 3, 1)]);
// Min cut should be 1 again
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
_ => panic!("Expected ValueInRange"),
}
}
#[test]
fn test_range_bounds() {
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
// Instance with range [2, 5]
let mut instance = StubInstance::new(&graph, 2, 5);
// Min cut is 1, which is below range [2,5], but stub only checks <= lambda_max
// so it returns ValueInRange
let result = instance.query();
// Stub doesn't check lambda_min, so behavior depends on implementation
// Instance with range [0, 1]
let mut instance = StubInstance::new(&graph, 0, 1);
// Min cut is 1, which is in range
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
_ => panic!("Expected ValueInRange"),
}
// Instance with range [0, 0]
let mut instance = StubInstance::new(&graph, 0, 0);
// Min cut is 1, which is above range
let result = instance.query();
assert!(matches!(result, InstanceResult::AboveRange));
}
#[test]
fn test_witness_information() {
let graph = DynamicGraph::new();
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
let mut instance = StubInstance::new(&graph, 0, 10);
let result = instance.query();
match result {
InstanceResult::ValueInRange { value, witness } => {
assert_eq!(value, 1);
assert_eq!(witness.boundary_size(), 1);
assert!(witness.cardinality() > 0);
assert!(witness.cardinality() < 3); // Proper cut
}
_ => panic!("Expected ValueInRange with witness"),
}
}
}

View File

@@ -0,0 +1,236 @@
//! Core traits for bounded-range minimum cut instances
//!
//! This module defines the `ProperCutInstance` trait that all bounded-range
//! minimum cut solvers must implement. The trait provides a unified interface
//! for maintaining minimum proper cuts under dynamic edge updates.
//!
//! # Overview
//!
//! A **proper cut instance** maintains the minimum proper cut for a graph
//! under the assumption that the minimum cut value λ ∈ [λ_min, λ_max].
//! This bounded assumption enables more efficient algorithms than maintaining
//! the exact minimum cut for arbitrary λ values.
//!
//! # Guarantees
//!
//! - **Correctness**: If λ ∈ [λ_min, λ_max], the instance returns correct results
//! - **Undefined behavior**: If λ < λ_min, behavior is undefined
//! - **Detection**: If λ > λ_max, the instance reports `AboveRange`
//!
//! # Update Model
//!
//! Updates follow a two-phase protocol:
//! 1. **Insert phase**: Call `apply_inserts()` with new edges
//! 2. **Delete phase**: Call `apply_deletes()` with removed edges
//!
//! This ordering ensures graph connectivity is maintained during updates.
use super::witness::WitnessHandle;
use crate::graph::{DynamicGraph, EdgeId, VertexId};
/// Result from a bounded-range instance query
///
/// Represents the outcome of querying a minimum proper cut instance.
/// The instance either finds a cut within the bounded range [λ_min, λ_max]
/// or determines that the minimum cut exceeds λ_max.
#[derive(Debug, Clone)]
pub enum InstanceResult {
/// Cut value is within [λ_min, λ_max], with witness
///
/// The witness certifies that a proper cut exists with the given value.
/// The value is guaranteed to be in the range [λ_min, λ_max].
///
/// # Fields
///
/// - `value`: The cut value |δ(U)| where U is the witness set
/// - `witness`: A witness handle certifying the cut
ValueInRange {
/// The minimum proper cut value
value: u64,
/// Witness certifying the cut
witness: WitnessHandle,
},
/// Cut value exceeds λ_max
///
/// The instance has detected that the minimum proper cut value
/// is strictly greater than λ_max. No witness is provided because
/// maintaining witnesses above the range is not required.
///
/// This typically triggers a range adjustment in the outer algorithm.
AboveRange,
}
impl InstanceResult {
/// Check if result is in range
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::traits::InstanceResult;
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(0, RoaringBitmap::from_iter([0, 1]), 5);
/// let result = InstanceResult::ValueInRange { value: 5, witness };
/// assert!(result.is_in_range());
///
/// let result = InstanceResult::AboveRange;
/// assert!(!result.is_in_range());
/// ```
pub fn is_in_range(&self) -> bool {
matches!(self, InstanceResult::ValueInRange { .. })
}
/// Check if result is above range
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::traits::InstanceResult;
///
/// let result = InstanceResult::AboveRange;
/// assert!(result.is_above_range());
/// ```
pub fn is_above_range(&self) -> bool {
matches!(self, InstanceResult::AboveRange)
}
/// Get the cut value if in range
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::traits::InstanceResult;
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(0, RoaringBitmap::from_iter([0]), 7);
/// let result = InstanceResult::ValueInRange { value: 7, witness };
/// assert_eq!(result.value(), Some(7));
///
/// let result = InstanceResult::AboveRange;
/// assert_eq!(result.value(), None);
/// ```
pub fn value(&self) -> Option<u64> {
match self {
InstanceResult::ValueInRange { value, .. } => Some(*value),
InstanceResult::AboveRange => None,
}
}
/// Get the witness if in range
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::traits::InstanceResult;
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(0, RoaringBitmap::from_iter([0]), 7);
/// let result = InstanceResult::ValueInRange { value: 7, witness: witness.clone() };
/// assert!(result.witness().is_some());
///
/// let result = InstanceResult::AboveRange;
/// assert!(result.witness().is_none());
/// ```
pub fn witness(&self) -> Option<&WitnessHandle> {
match self {
InstanceResult::ValueInRange { witness, .. } => Some(witness),
InstanceResult::AboveRange => None,
}
}
}
/// A bounded-range proper cut instance
///
/// This trait defines the interface for maintaining minimum proper cuts
/// over a dynamic graph, assuming the cut value λ remains within a
/// bounded range [λ_min, λ_max].
///
/// # Proper Cuts
///
/// A **proper cut** is a partition (U, V \ U) where both U and V \ U
/// induce connected subgraphs. This is stricter than a general cut.
///
/// # Bounded Range Assumption
///
/// The instance assumes λ ∈ [λ_min, λ_max]:
/// - If λ < λ_min: Undefined behavior
/// - If λ ∈ [λ_min, λ_max]: Returns `ValueInRange` with witness
/// - If λ > λ_max: Returns `AboveRange`
///
/// # Update Protocol
///
/// Updates must follow this order:
/// 1. Call `apply_inserts()` with batch of insertions
/// 2. Call `apply_deletes()` with batch of deletions
/// 3. Call `query()` to get updated result
///
/// # Thread Safety
///
/// Implementations must be `Send + Sync` for use in parallel algorithms.
pub trait ProperCutInstance: Send + Sync {
/// Initialize instance on graph with given bounds
///
/// Creates a new instance that maintains minimum proper cuts
/// for the given graph, assuming λ ∈ [λ_min, λ_max].
///
/// # Arguments
///
/// * `graph` - The dynamic graph to operate on
/// * `lambda_min` - Minimum bound on the cut value
/// * `lambda_max` - Maximum bound on the cut value
///
/// # Panics
///
/// May panic if λ_min > λ_max or if the graph is invalid.
fn init(graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self
where
Self: Sized;
/// Apply batch of edge insertions
///
/// Inserts a batch of edges into the maintained structure.
/// Must be called **before** `apply_deletes()` in each update round.
///
/// # Arguments
///
/// * `edges` - Slice of (edge_id, source, target) tuples to insert
fn apply_inserts(&mut self, edges: &[(EdgeId, VertexId, VertexId)]);
/// Apply batch of edge deletions
///
/// Deletes a batch of edges from the maintained structure.
/// Must be called **after** `apply_inserts()` in each update round.
///
/// # Arguments
///
/// * `edges` - Slice of (edge_id, source, target) tuples to delete
fn apply_deletes(&mut self, edges: &[(EdgeId, VertexId, VertexId)]);
/// Query current minimum proper cut
///
/// Returns the current minimum proper cut value and witness,
/// or indicates that the cut value exceeds the maximum bound.
///
/// # Returns
///
/// - `ValueInRange { value, witness }` if λ ∈ [λ_min, λ_max]
/// - `AboveRange` if λ > λ_max
///
/// # Complexity
///
/// Typically O(1) to O(log n) depending on the data structure.
fn query(&mut self) -> InstanceResult;
/// Get the lambda bounds for this instance
///
/// Returns the [λ_min, λ_max] bounds this instance was initialized with.
///
/// # Returns
///
/// A tuple (λ_min, λ_max)
fn bounds(&self) -> (u64, u64);
}

View File

@@ -0,0 +1,661 @@
//! Witness types for cut certification
//!
//! A witness represents a connected set U ⊆ V with its boundary δ(U).
//! The witness certifies that a proper cut exists with value |δ(U)|.
//!
//! # Representation
//!
//! Witnesses use an implicit representation for memory efficiency:
//! - **Seed vertex**: The starting vertex that defines the connected component
//! - **Membership bitmap**: Compressed bitmap indicating which vertices are in U
//! - **Boundary size**: Pre-computed value |δ(U)| for O(1) queries
//! - **Hash**: Fast equality checking without full comparison
//!
//! # Performance
//!
//! - `WitnessHandle` uses `Arc` for cheap cloning (O(1))
//! - `contains()` is O(1) via bitmap lookup
//! - `boundary_size()` is O(1) via cached value
//! - `materialize_partition()` is O(|V|) and should be used sparingly
use crate::graph::VertexId;
use roaring::RoaringBitmap;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
/// Handle to a witness (cheap to clone)
///
/// This is the primary type for passing witnesses around. It uses an `Arc`
/// internally so cloning is O(1) and witnesses can be shared across threads.
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let mut membership = RoaringBitmap::new();
/// membership.insert(1);
/// membership.insert(2);
/// membership.insert(3);
///
/// let witness = WitnessHandle::new(1, membership, 4);
/// assert!(witness.contains(1));
/// assert!(witness.contains(2));
/// assert!(!witness.contains(5));
/// assert_eq!(witness.boundary_size(), 4);
/// ```
#[derive(Debug, Clone)]
pub struct WitnessHandle {
inner: Arc<ImplicitWitness>,
}
/// Implicit representation of a cut witness
///
/// The witness represents a connected set U ⊆ V where:
/// - U contains the seed vertex
/// - |δ(U)| = boundary_size
/// - membership\[v\] = true iff v ∈ U
#[derive(Debug)]
pub struct ImplicitWitness {
/// Seed vertex that defines the cut (always in U)
pub seed: VertexId,
/// Membership bitmap (vertex v is in U iff bit v is set)
pub membership: RoaringBitmap,
/// Current boundary size |δ(U)|
pub boundary_size: u64,
/// Hash for quick equality checks
pub hash: u64,
}
impl WitnessHandle {
/// Create a new witness handle
///
/// # Arguments
///
/// * `seed` - The seed vertex defining this cut (must be in membership)
/// * `membership` - Bitmap of vertices in the cut set U
/// * `boundary_size` - The size of the boundary |δ(U)|
///
/// # Panics
///
/// Panics if the seed vertex is not in the membership set (debug builds only)
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let mut membership = RoaringBitmap::new();
/// membership.insert(0);
/// membership.insert(1);
///
/// let witness = WitnessHandle::new(0, membership, 5);
/// assert_eq!(witness.seed(), 0);
/// ```
pub fn new(seed: VertexId, membership: RoaringBitmap, boundary_size: u64) -> Self {
debug_assert!(
seed <= u32::MAX as u64,
"Seed vertex {} exceeds u32::MAX",
seed
);
debug_assert!(
membership.contains(seed as u32),
"Seed vertex {} must be in membership set",
seed
);
let hash = Self::compute_hash(seed, &membership);
Self {
inner: Arc::new(ImplicitWitness {
seed,
membership,
boundary_size,
hash,
}),
}
}
/// Compute hash for a witness
///
/// The hash combines the seed vertex and membership bitmap for fast equality checks.
fn compute_hash(seed: VertexId, membership: &RoaringBitmap) -> u64 {
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
// Hash the membership bitmap by iterating its values
for vertex in membership.iter() {
vertex.hash(&mut hasher);
}
hasher.finish()
}
/// Check if vertex is in the cut set U
///
/// # Time Complexity
///
/// O(1) via bitmap lookup
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let mut membership = RoaringBitmap::new();
/// membership.insert(5);
/// membership.insert(10);
///
/// let witness = WitnessHandle::new(5, membership, 3);
/// assert!(witness.contains(5));
/// assert!(witness.contains(10));
/// assert!(!witness.contains(15));
/// ```
#[inline]
pub fn contains(&self, v: VertexId) -> bool {
if v > u32::MAX as u64 {
return false;
}
self.inner.membership.contains(v as u32)
}
/// Get boundary size |δ(U)|
///
/// Returns the pre-computed boundary size for O(1) access.
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1, 2, 3]), 7);
/// assert_eq!(witness.boundary_size(), 7);
/// ```
#[inline]
pub fn boundary_size(&self) -> u64 {
self.inner.boundary_size
}
/// Get the seed vertex
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(42, RoaringBitmap::from_iter([42u32]), 1);
/// assert_eq!(witness.seed(), 42);
/// ```
#[inline]
pub fn seed(&self) -> VertexId {
self.inner.seed
}
/// Get the witness hash
///
/// Used for fast equality checks without comparing full membership sets.
#[inline]
pub fn hash(&self) -> u64 {
self.inner.hash
}
/// Materialize full partition (U, V \ U)
///
/// This is an expensive operation (O(|V|)) that converts the implicit
/// representation into explicit sets. Use sparingly, primarily for
/// debugging or verification.
///
/// # Returns
///
/// A tuple `(U, V_minus_U)` where:
/// - `U` is the set of vertices in the cut
/// - `V_minus_U` is the complement set
///
/// # Note
///
/// This method assumes vertices are numbered 0..max_vertex. For sparse
/// graphs, V \ U may contain vertex IDs that don't exist in the graph.
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
/// use std::collections::HashSet;
///
/// let mut membership = RoaringBitmap::new();
/// membership.insert(1);
/// membership.insert(2);
///
/// let witness = WitnessHandle::new(1, membership, 3);
/// let (u, _v_minus_u) = witness.materialize_partition();
///
/// assert!(u.contains(&1));
/// assert!(u.contains(&2));
/// assert!(!u.contains(&3));
/// ```
pub fn materialize_partition(&self) -> (HashSet<VertexId>, HashSet<VertexId>) {
let u: HashSet<VertexId> = self.inner.membership.iter().map(|v| v as u64).collect();
// Find the maximum vertex ID to determine graph size
let max_vertex = self.inner.membership.max().unwrap_or(0) as u64;
// Create complement set
let v_minus_u: HashSet<VertexId> = (0..=max_vertex)
.filter(|&v| !self.inner.membership.contains(v as u32))
.collect();
(u, v_minus_u)
}
/// Get the cardinality of the cut set U
///
/// # Examples
///
/// ```
/// use ruvector_mincut::instance::witness::WitnessHandle;
/// use roaring::RoaringBitmap;
///
/// let witness = WitnessHandle::new(1, RoaringBitmap::from_iter([1u32, 2u32, 3u32]), 5);
/// assert_eq!(witness.cardinality(), 3);
/// ```
#[inline]
pub fn cardinality(&self) -> u64 {
self.inner.membership.len()
}
}
impl PartialEq for WitnessHandle {
/// Fast equality check using hash
///
/// First compares hashes (O(1)), then falls back to full comparison if needed.
fn eq(&self, other: &Self) -> bool {
// Fast path: compare hashes
if self.inner.hash != other.inner.hash {
return false;
}
// Slow path: compare actual membership
self.inner.seed == other.inner.seed
&& self.inner.boundary_size == other.inner.boundary_size
&& self.inner.membership == other.inner.membership
}
}
impl Eq for WitnessHandle {}
/// Trait for witness operations
///
/// This trait abstracts witness operations for generic programming.
/// The primary implementation is `WitnessHandle`.
pub trait Witness {
/// Check if vertex is in the cut set U
fn contains(&self, v: VertexId) -> bool;
/// Get boundary size |δ(U)|
fn boundary_size(&self) -> u64;
/// Materialize full partition (expensive)
fn materialize_partition(&self) -> (HashSet<VertexId>, HashSet<VertexId>);
/// Get the seed vertex
fn seed(&self) -> VertexId;
/// Get cardinality of U
fn cardinality(&self) -> u64;
}
impl Witness for WitnessHandle {
#[inline]
fn contains(&self, v: VertexId) -> bool {
WitnessHandle::contains(self, v)
}
#[inline]
fn boundary_size(&self) -> u64 {
WitnessHandle::boundary_size(self)
}
fn materialize_partition(&self) -> (HashSet<VertexId>, HashSet<VertexId>) {
WitnessHandle::materialize_partition(self)
}
#[inline]
fn seed(&self) -> VertexId {
WitnessHandle::seed(self)
}
#[inline]
fn cardinality(&self) -> u64 {
WitnessHandle::cardinality(self)
}
}
/// Recipe for constructing a witness lazily
///
/// Instead of computing the full membership bitmap upfront, this struct
/// stores the parameters needed to construct it on demand. This is useful
/// when you need to store many potential witnesses but only access a few.
///
/// # Lazy Evaluation
///
/// The membership bitmap is only computed when:
/// - `materialize()` is called to get a full `WitnessHandle`
/// - `contains()` is called to check vertex membership
/// - Any other operation that requires the full witness
///
/// # Memory Savings
///
/// A `LazyWitness` uses only ~32 bytes vs potentially kilobytes for a
/// `WitnessHandle` with a large membership bitmap.
///
/// # Example
///
/// ```
/// use ruvector_mincut::instance::witness::LazyWitness;
///
/// // Create a lazy witness recipe
/// let lazy = LazyWitness::new(42, 10, 5);
///
/// // No computation happens until materialized
/// assert_eq!(lazy.seed(), 42);
/// assert_eq!(lazy.boundary_size(), 5);
///
/// // Calling with_adjacency materializes the witness
/// // (requires adjacency data from the graph)
/// ```
#[derive(Debug, Clone)]
pub struct LazyWitness {
/// Seed vertex that defines the cut
seed: VertexId,
/// Radius of the local search that found this witness
radius: usize,
/// Pre-computed boundary size
boundary_size: u64,
/// Cached materialized witness (computed on first access)
cached: std::sync::OnceLock<WitnessHandle>,
}
impl LazyWitness {
/// Create a new lazy witness
///
/// # Arguments
///
/// * `seed` - Seed vertex defining the cut
/// * `radius` - Radius of local search used
/// * `boundary_size` - Pre-computed boundary size
pub fn new(seed: VertexId, radius: usize, boundary_size: u64) -> Self {
Self {
seed,
radius,
boundary_size,
cached: std::sync::OnceLock::new(),
}
}
/// Get the seed vertex
#[inline]
pub fn seed(&self) -> VertexId {
self.seed
}
/// Get the search radius
#[inline]
pub fn radius(&self) -> usize {
self.radius
}
/// Get boundary size |δ(U)|
#[inline]
pub fn boundary_size(&self) -> u64 {
self.boundary_size
}
/// Check if the witness has been materialized
#[inline]
pub fn is_materialized(&self) -> bool {
self.cached.get().is_some()
}
/// Materialize the witness with adjacency information
///
/// This performs a BFS from the seed vertex up to the given radius
/// to construct the full membership bitmap.
///
/// # Arguments
///
/// * `adjacency` - Function to get neighbors of a vertex
///
/// # Returns
///
/// A fully materialized `WitnessHandle`
pub fn materialize<F>(&self, adjacency: F) -> WitnessHandle
where
F: Fn(VertexId) -> Vec<VertexId>,
{
self.cached
.get_or_init(|| {
// BFS from seed up to radius
let mut membership = RoaringBitmap::new();
let mut visited = HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back((self.seed, 0usize));
visited.insert(self.seed);
membership.insert(self.seed as u32);
while let Some((vertex, dist)) = queue.pop_front() {
if dist >= self.radius {
continue;
}
for neighbor in adjacency(vertex) {
if visited.insert(neighbor) {
membership.insert(neighbor as u32);
queue.push_back((neighbor, dist + 1));
}
}
}
WitnessHandle::new(self.seed, membership, self.boundary_size)
})
.clone()
}
/// Set a pre-computed witness (for cases where we already have it)
pub fn set_materialized(&self, witness: WitnessHandle) {
let _ = self.cached.set(witness);
}
/// Get the cached witness if already materialized
pub fn get_cached(&self) -> Option<&WitnessHandle> {
self.cached.get()
}
}
/// Batch of lazy witnesses for efficient storage
///
/// Stores multiple lazy witnesses compactly and tracks which
/// have been materialized.
#[derive(Debug, Default)]
pub struct LazyWitnessBatch {
/// Lazy witnesses in this batch
witnesses: Vec<LazyWitness>,
/// Count of materialized witnesses
materialized_count: std::sync::atomic::AtomicUsize,
}
impl LazyWitnessBatch {
/// Create a new empty batch
pub fn new() -> Self {
Self {
witnesses: Vec::new(),
materialized_count: std::sync::atomic::AtomicUsize::new(0),
}
}
/// Create batch with capacity
pub fn with_capacity(capacity: usize) -> Self {
Self {
witnesses: Vec::with_capacity(capacity),
materialized_count: std::sync::atomic::AtomicUsize::new(0),
}
}
/// Add a lazy witness to the batch
pub fn push(&mut self, witness: LazyWitness) {
self.witnesses.push(witness);
}
/// Get witness by index
pub fn get(&self, index: usize) -> Option<&LazyWitness> {
self.witnesses.get(index)
}
/// Number of witnesses in batch
pub fn len(&self) -> usize {
self.witnesses.len()
}
/// Check if batch is empty
pub fn is_empty(&self) -> bool {
self.witnesses.is_empty()
}
/// Count of materialized witnesses
pub fn materialized_count(&self) -> usize {
self.materialized_count
.load(std::sync::atomic::Ordering::Relaxed)
}
/// Materialize a specific witness
pub fn materialize<F>(&self, index: usize, adjacency: F) -> Option<WitnessHandle>
where
F: Fn(VertexId) -> Vec<VertexId>,
{
self.witnesses.get(index).map(|lazy| {
let was_materialized = lazy.is_materialized();
let handle = lazy.materialize(adjacency);
if !was_materialized {
self.materialized_count
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
handle
})
}
/// Find witness with smallest boundary (materializes only as needed)
pub fn find_smallest_boundary(&self) -> Option<&LazyWitness> {
self.witnesses.iter().min_by_key(|w| w.boundary_size())
}
/// Iterate over all lazy witnesses
pub fn iter(&self) -> impl Iterator<Item = &LazyWitness> {
self.witnesses.iter()
}
}
#[cfg(test)]
mod lazy_tests {
use super::*;
#[test]
fn test_lazy_witness_new() {
let lazy = LazyWitness::new(42, 5, 10);
assert_eq!(lazy.seed(), 42);
assert_eq!(lazy.radius(), 5);
assert_eq!(lazy.boundary_size(), 10);
assert!(!lazy.is_materialized());
}
#[test]
fn test_lazy_witness_materialize() {
let lazy = LazyWitness::new(0, 2, 3);
// Simple adjacency: linear graph 0-1-2-3-4
let adjacency = |v: VertexId| -> Vec<VertexId> {
match v {
0 => vec![1],
1 => vec![0, 2],
2 => vec![1, 3],
3 => vec![2, 4],
4 => vec![3],
_ => vec![],
}
};
let handle = lazy.materialize(adjacency);
// With radius 2 from vertex 0, should include 0, 1, 2
assert!(handle.contains(0));
assert!(handle.contains(1));
assert!(handle.contains(2));
assert!(!handle.contains(3)); // Beyond radius 2
assert!(lazy.is_materialized());
}
#[test]
fn test_lazy_witness_caching() {
let lazy = LazyWitness::new(0, 1, 5);
let call_count = std::sync::atomic::AtomicUsize::new(0);
let adjacency = |v: VertexId| -> Vec<VertexId> {
call_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if v == 0 {
vec![1, 2]
} else {
vec![]
}
};
// First materialization
let _h1 = lazy.materialize(&adjacency);
let first_count = call_count.load(std::sync::atomic::Ordering::Relaxed);
// Second materialization should use cache
let _h2 = lazy.materialize(&adjacency);
let second_count = call_count.load(std::sync::atomic::Ordering::Relaxed);
// Adjacency should only be called during first materialization
assert_eq!(first_count, second_count);
}
#[test]
fn test_lazy_witness_batch() {
let mut batch = LazyWitnessBatch::with_capacity(3);
batch.push(LazyWitness::new(0, 2, 5));
batch.push(LazyWitness::new(1, 3, 3)); // Smallest boundary
batch.push(LazyWitness::new(2, 1, 7));
assert_eq!(batch.len(), 3);
assert_eq!(batch.materialized_count(), 0);
// Find smallest boundary
let smallest = batch.find_smallest_boundary().unwrap();
assert_eq!(smallest.seed(), 1);
assert_eq!(smallest.boundary_size(), 3);
}
#[test]
fn test_batch_materialize() {
let mut batch = LazyWitnessBatch::new();
batch.push(LazyWitness::new(0, 1, 5));
let adjacency = |_v: VertexId| -> Vec<VertexId> { vec![1, 2] };
let handle = batch.materialize(0, adjacency).unwrap();
assert!(handle.contains(0));
assert!(handle.contains(1));
assert!(handle.contains(2));
assert_eq!(batch.materialized_count(), 1);
}
}