Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
596
vendor/ruvector/crates/ruvector-mincut/src/instance/bounded.rs
vendored
Normal file
596
vendor/ruvector/crates/ruvector-mincut/src/instance/bounded.rs
vendored
Normal 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(¤t) {
|
||||
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(¤t) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
43
vendor/ruvector/crates/ruvector-mincut/src/instance/mod.rs
vendored
Normal file
43
vendor/ruvector/crates/ruvector-mincut/src/instance/mod.rs
vendored
Normal 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));
|
||||
}
|
||||
}
|
||||
557
vendor/ruvector/crates/ruvector-mincut/src/instance/stub.rs
vendored
Normal file
557
vendor/ruvector/crates/ruvector-mincut/src/instance/stub.rs
vendored
Normal 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(¤t) {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
236
vendor/ruvector/crates/ruvector-mincut/src/instance/traits.rs
vendored
Normal file
236
vendor/ruvector/crates/ruvector-mincut/src/instance/traits.rs
vendored
Normal 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);
|
||||
}
|
||||
661
vendor/ruvector/crates/ruvector-mincut/src/instance/witness.rs
vendored
Normal file
661
vendor/ruvector/crates/ruvector-mincut/src/instance/witness.rs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user