Skip to main content

ruvector_mincut/instance/
bounded.rs

1//! Bounded-range instance using DeterministicLocalKCut
2//!
3//! Production implementation of ProperCutInstance that uses the
4//! deterministic local k-cut oracle from the paper.
5
6use super::witness::WitnessHandle;
7use super::{InstanceResult, ProperCutInstance};
8use crate::certificate::{
9    CertLocalKCutQuery, CutCertificate, LocalKCutResponse, LocalKCutResultSummary,
10};
11use crate::cluster::ClusterHierarchy;
12use crate::fragment::FragmentingAlgorithm;
13use crate::graph::{DynamicGraph, EdgeId, VertexId};
14use crate::localkcut::paper_impl::{
15    DeterministicLocalKCut, LocalKCutOracle, LocalKCutQuery, LocalKCutResult,
16};
17use roaring::RoaringBitmap;
18use std::collections::{HashMap, HashSet, VecDeque};
19use std::sync::{Arc, Mutex};
20
21/// Cached boundary value for incremental updates
22#[derive(Clone, Default)]
23struct BoundaryCache {
24    /// Cached boundary size
25    value: u64,
26    /// Whether the cache is valid
27    valid: bool,
28}
29
30/// Bounded-range instance using LocalKCut oracle
31///
32/// Maintains a family of candidate cuts and uses LocalKCut
33/// to find new cuts or certify none exist in the range.
34pub struct BoundedInstance {
35    /// Lambda bounds
36    lambda_min: u64,
37    lambda_max: u64,
38    /// Local graph copy (edges and vertices)
39    edges: Vec<(EdgeId, VertexId, VertexId)>,
40    vertices: HashSet<VertexId>,
41    /// Adjacency list
42    adjacency: HashMap<VertexId, Vec<(VertexId, EdgeId)>>,
43    /// Current best witness (cached with interior mutability)
44    best_witness: Mutex<Option<(u64, WitnessHandle)>>,
45    /// LocalKCut oracle
46    oracle: DeterministicLocalKCut,
47    /// Certificate for verification (interior mutability for query())
48    certificate: Mutex<CutCertificate>,
49    /// Maximum radius for local search
50    max_radius: usize,
51    /// Cluster hierarchy for strategic seed selection
52    cluster_hierarchy: Option<ClusterHierarchy>,
53    /// Fragmenting algorithm for disconnected graph handling
54    fragmenting: Option<FragmentingAlgorithm>,
55    /// Cached boundary for incremental updates (O(1) vs O(m))
56    boundary_cache: Mutex<BoundaryCache>,
57}
58
59impl BoundedInstance {
60    /// Create a new bounded instance
61    pub fn new(lambda_min: u64, lambda_max: u64) -> Self {
62        Self {
63            lambda_min,
64            lambda_max,
65            edges: Vec::new(),
66            vertices: HashSet::new(),
67            adjacency: HashMap::new(),
68            best_witness: Mutex::new(None),
69            oracle: DeterministicLocalKCut::new(20), // Default max radius
70            certificate: Mutex::new(CutCertificate::new()),
71            max_radius: 20,
72            cluster_hierarchy: None,
73            fragmenting: None,
74            boundary_cache: Mutex::new(BoundaryCache::default()),
75        }
76    }
77
78    /// Ensure cluster hierarchy is built when needed
79    fn ensure_hierarchy(&mut self, graph: &DynamicGraph) {
80        if self.cluster_hierarchy.is_none() && self.vertices.len() > 50 {
81            self.cluster_hierarchy = Some(ClusterHierarchy::new(Arc::new(graph.clone())));
82        }
83    }
84
85    /// Rebuild adjacency from edges
86    fn rebuild_adjacency(&mut self) {
87        self.adjacency.clear();
88        for &(edge_id, u, v) in &self.edges {
89            self.adjacency.entry(u).or_default().push((v, edge_id));
90            self.adjacency.entry(v).or_default().push((u, edge_id));
91        }
92    }
93
94    /// Insert an edge with incremental boundary update
95    fn insert(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
96        self.vertices.insert(u);
97        self.vertices.insert(v);
98        self.edges.push((edge_id, u, v));
99
100        self.adjacency.entry(u).or_default().push((v, edge_id));
101        self.adjacency.entry(v).or_default().push((u, edge_id));
102
103        // Incrementally update boundary cache if valid
104        self.update_boundary_on_insert(u, v);
105
106        // Invalidate witness if affected
107        self.maybe_invalidate_witness(u, v);
108    }
109
110    /// Delete an edge with incremental boundary update
111    fn delete(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
112        // Check if edge crosses cut before removing (for incremental update)
113        self.update_boundary_on_delete(u, v);
114
115        self.edges.retain(|(eid, _, _)| *eid != edge_id);
116        self.rebuild_adjacency();
117
118        // Invalidate current witness since structure changed
119        *self.best_witness.lock().unwrap() = None;
120        // Note: boundary cache is already updated incrementally above
121    }
122
123    /// Incrementally update boundary cache on edge insertion
124    fn update_boundary_on_insert(&self, u: VertexId, v: VertexId) {
125        let witness_ref = self.best_witness.lock().unwrap();
126        if let Some((_, ref witness)) = *witness_ref {
127            let u_in = witness.contains(u);
128            let v_in = witness.contains(v);
129
130            // If edge crosses the cut, increment boundary
131            if u_in != v_in {
132                let mut cache = self.boundary_cache.lock().unwrap();
133                if cache.valid {
134                    cache.value += 1;
135                }
136            }
137        }
138    }
139
140    /// Incrementally update boundary cache on edge deletion
141    fn update_boundary_on_delete(&self, u: VertexId, v: VertexId) {
142        let witness_ref = self.best_witness.lock().unwrap();
143        if let Some((_, ref witness)) = *witness_ref {
144            let u_in = witness.contains(u);
145            let v_in = witness.contains(v);
146
147            // If edge crossed the cut, decrement boundary
148            if u_in != v_in {
149                let mut cache = self.boundary_cache.lock().unwrap();
150                if cache.valid {
151                    cache.value = cache.value.saturating_sub(1);
152                }
153            }
154        }
155    }
156
157    /// Check if witness needs invalidation after edge change
158    fn maybe_invalidate_witness(&mut self, u: VertexId, v: VertexId) {
159        let mut witness_ref = self.best_witness.lock().unwrap();
160        if let Some((_, ref witness)) = *witness_ref {
161            let u_in = witness.contains(u);
162            let v_in = witness.contains(v);
163
164            // If edge crosses the cut boundary, witness becomes invalid
165            // Note: boundary was already incrementally updated, but witness value is now stale
166            if u_in != v_in {
167                *witness_ref = None;
168                // Also invalidate boundary cache since we no longer have a valid witness
169                drop(witness_ref); // Release lock before acquiring another
170                self.invalidate_boundary_cache();
171            }
172        }
173    }
174
175    /// Check if graph is connected
176    fn is_connected(&self) -> bool {
177        if self.vertices.is_empty() {
178            return true;
179        }
180
181        let start = *self.vertices.iter().next().unwrap();
182        let mut visited = HashSet::new();
183        let mut queue = VecDeque::new();
184
185        queue.push_back(start);
186        visited.insert(start);
187
188        while let Some(current) = queue.pop_front() {
189            if let Some(neighbors) = self.adjacency.get(&current) {
190                for &(neighbor, _) in neighbors {
191                    if visited.insert(neighbor) {
192                        queue.push_back(neighbor);
193                    }
194                }
195            }
196        }
197
198        visited.len() == self.vertices.len()
199    }
200
201    /// Search for cuts using LocalKCut oracle
202    fn search_for_cuts(&mut self) -> Option<(u64, WitnessHandle)> {
203        // Build a temporary graph for the oracle
204        let graph = Arc::new(DynamicGraph::new());
205        for &(_, u, v) in &self.edges {
206            let _ = graph.insert_edge(u, v, 1.0);
207        }
208
209        // Build cluster hierarchy for strategic seed selection
210        self.ensure_hierarchy(&graph);
211
212        // Determine seed vertices to try
213        let seed_vertices: Vec<VertexId> = if let Some(ref hierarchy) = self.cluster_hierarchy {
214            // Use cluster boundary vertices as strategic seeds
215            let mut boundary_vertices = HashSet::new();
216
217            // Collect vertices from cluster boundaries
218            for cluster in hierarchy.clusters.values() {
219                // Get vertices on the boundary of each cluster
220                for &v in &cluster.vertices {
221                    if let Some(neighbors) = self.adjacency.get(&v) {
222                        for &(neighbor, _) in neighbors {
223                            // If neighbor is outside cluster, v is on boundary
224                            if !cluster.vertices.contains(&neighbor) {
225                                boundary_vertices.insert(v);
226                            }
227                        }
228                    }
229                }
230            }
231
232            // If we have boundary vertices, use them; otherwise fall back to all vertices
233            if boundary_vertices.is_empty() {
234                self.vertices.iter().copied().collect()
235            } else {
236                boundary_vertices.into_iter().collect()
237            }
238        } else {
239            // No hierarchy - use all vertices
240            self.vertices.iter().copied().collect()
241        };
242
243        // Try different budgets within our range
244        for budget in self.lambda_min..=self.lambda_max {
245            // Try strategic seed vertices
246            for &seed in &seed_vertices {
247                let query = LocalKCutQuery {
248                    seed_vertices: vec![seed],
249                    budget_k: budget,
250                    radius: self.max_radius,
251                };
252
253                // Log the query
254                self.certificate
255                    .lock()
256                    .unwrap()
257                    .add_response(LocalKCutResponse {
258                        query: CertLocalKCutQuery {
259                            seed_vertices: vec![seed],
260                            budget_k: budget,
261                            radius: self.max_radius,
262                        },
263                        result: LocalKCutResultSummary::NoneInLocality,
264                        timestamp: 0,
265                        trigger: None,
266                    });
267
268                match self.oracle.search(&graph, query) {
269                    LocalKCutResult::Found { witness, cut_value } => {
270                        // Update certificate
271                        let mut cert = self.certificate.lock().unwrap();
272                        if let Some(last) = cert.localkcut_responses.last_mut() {
273                            last.result = LocalKCutResultSummary::Found {
274                                cut_value,
275                                witness_hash: witness.hash(),
276                            };
277                        }
278
279                        if cut_value >= self.lambda_min && cut_value <= self.lambda_max {
280                            return Some((cut_value, witness));
281                        }
282                    }
283                    LocalKCutResult::NoneInLocality => {
284                        // Continue searching
285                    }
286                }
287            }
288        }
289
290        None
291    }
292
293    /// Compute minimum cut (for small graphs or fallback)
294    fn brute_force_min_cut(&self) -> Option<(u64, WitnessHandle)> {
295        if self.vertices.len() >= 20 {
296            return None;
297        }
298
299        let vertex_vec: Vec<_> = self.vertices.iter().copied().collect();
300        let n = vertex_vec.len();
301
302        if n <= 1 {
303            return None;
304        }
305
306        let mut min_cut = u64::MAX;
307        let mut best_set = HashSet::new();
308
309        let max_mask = 1u64 << n;
310        for mask in 1..max_mask - 1 {
311            let mut subset = HashSet::new();
312            for (i, &v) in vertex_vec.iter().enumerate() {
313                if mask & (1 << i) != 0 {
314                    subset.insert(v);
315                }
316            }
317
318            // Check connectivity
319            if !self.is_subset_connected(&subset) {
320                continue;
321            }
322
323            // Compute boundary
324            let boundary = self.compute_boundary(&subset);
325
326            if boundary < min_cut {
327                min_cut = boundary;
328                best_set = subset;
329            }
330        }
331
332        if min_cut == u64::MAX || best_set.is_empty() {
333            return None;
334        }
335
336        let membership: RoaringBitmap = best_set.iter().map(|&v| v as u32).collect();
337        let seed = *best_set.iter().next().unwrap();
338        let witness = WitnessHandle::new(seed, membership, min_cut);
339
340        Some((min_cut, witness))
341    }
342
343    /// Check if subset is connected
344    fn is_subset_connected(&self, subset: &HashSet<VertexId>) -> bool {
345        if subset.is_empty() {
346            return true;
347        }
348
349        let start = *subset.iter().next().unwrap();
350        let mut visited = HashSet::new();
351        let mut queue = VecDeque::new();
352
353        queue.push_back(start);
354        visited.insert(start);
355
356        while let Some(current) = queue.pop_front() {
357            if let Some(neighbors) = self.adjacency.get(&current) {
358                for &(neighbor, _) in neighbors {
359                    if subset.contains(&neighbor) && visited.insert(neighbor) {
360                        queue.push_back(neighbor);
361                    }
362                }
363            }
364        }
365
366        visited.len() == subset.len()
367    }
368
369    /// Compute boundary of subset (O(m) operation)
370    fn compute_boundary(&self, subset: &HashSet<VertexId>) -> u64 {
371        let mut boundary = 0u64;
372
373        for &(_, u, v) in &self.edges {
374            let u_in = subset.contains(&u);
375            let v_in = subset.contains(&v);
376            if u_in != v_in {
377                boundary += 1;
378            }
379        }
380
381        boundary
382    }
383
384    /// Get cached boundary value for current witness
385    ///
386    /// # Performance
387    /// Returns O(1) if cache is valid, otherwise recomputes in O(m)
388    /// and caches the result for future incremental updates.
389    fn get_cached_boundary(&self) -> Option<u64> {
390        let cache = self.boundary_cache.lock().unwrap();
391        if cache.valid {
392            Some(cache.value)
393        } else {
394            None
395        }
396    }
397
398    /// Set boundary cache with new value
399    fn set_boundary_cache(&self, value: u64) {
400        let mut cache = self.boundary_cache.lock().unwrap();
401        cache.value = value;
402        cache.valid = true;
403    }
404
405    /// Invalidate boundary cache
406    fn invalidate_boundary_cache(&self) {
407        let mut cache = self.boundary_cache.lock().unwrap();
408        cache.valid = false;
409    }
410
411    /// Get the certificate
412    pub fn certificate(&self) -> CutCertificate {
413        self.certificate.lock().unwrap().clone()
414    }
415}
416
417impl ProperCutInstance for BoundedInstance {
418    fn init(_graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
419        Self::new(lambda_min, lambda_max)
420    }
421
422    fn apply_inserts(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
423        for &(edge_id, u, v) in edges {
424            self.insert(edge_id, u, v);
425        }
426    }
427
428    fn apply_deletes(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
429        for &(edge_id, u, v) in edges {
430            self.delete(edge_id, u, v);
431        }
432    }
433
434    fn query(&mut self) -> InstanceResult {
435        // FIRST: Check if graph is fragmented (disconnected) using FragmentingAlgorithm
436        if let Some(ref frag) = self.fragmenting {
437            if !frag.is_connected() {
438                // Graph is disconnected, min cut is 0
439                let v = *self.vertices.iter().next().unwrap_or(&0);
440                let mut membership = RoaringBitmap::new();
441                membership.insert(v as u32);
442                let witness = WitnessHandle::new(v, membership, 0);
443                return InstanceResult::ValueInRange { value: 0, witness };
444            }
445        } else {
446            // Fallback: Check for disconnected graph using basic connectivity check
447            if !self.is_connected() && !self.vertices.is_empty() {
448                let v = *self.vertices.iter().next().unwrap();
449                let mut membership = RoaringBitmap::new();
450                membership.insert(v as u32);
451                let witness = WitnessHandle::new(v, membership, 0);
452                return InstanceResult::ValueInRange { value: 0, witness };
453            }
454        }
455
456        // Use cached witness if valid
457        {
458            let witness_ref = self.best_witness.lock().unwrap();
459            if let Some((value, ref witness)) = *witness_ref {
460                if value >= self.lambda_min && value <= self.lambda_max {
461                    return InstanceResult::ValueInRange {
462                        value,
463                        witness: witness.clone(),
464                    };
465                }
466            }
467        }
468
469        // For small graphs, use brute force
470        if self.vertices.len() < 20 {
471            if let Some((value, witness)) = self.brute_force_min_cut() {
472                // Cache the result and initialize boundary cache for incremental updates
473                *self.best_witness.lock().unwrap() = Some((value, witness.clone()));
474                self.set_boundary_cache(value);
475
476                if value <= self.lambda_max {
477                    return InstanceResult::ValueInRange { value, witness };
478                } else {
479                    return InstanceResult::AboveRange;
480                }
481            }
482        }
483
484        // Use LocalKCut oracle for larger graphs
485        if let Some((value, witness)) = self.search_for_cuts() {
486            // Cache the result and initialize boundary cache for incremental updates
487            *self.best_witness.lock().unwrap() = Some((value, witness.clone()));
488            self.set_boundary_cache(value);
489            return InstanceResult::ValueInRange { value, witness };
490        }
491
492        // If no cut found in range, assume above range
493        InstanceResult::AboveRange
494    }
495
496    fn bounds(&self) -> (u64, u64) {
497        (self.lambda_min, self.lambda_max)
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    #[test]
506    fn test_empty_instance() {
507        let instance = BoundedInstance::new(1, 10);
508        assert_eq!(instance.bounds(), (1, 10));
509    }
510
511    #[test]
512    fn test_path_graph() {
513        let mut instance = BoundedInstance::new(0, 10);
514        instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
515
516        match instance.query() {
517            InstanceResult::ValueInRange { value, .. } => {
518                assert_eq!(value, 1);
519            }
520            _ => panic!("Expected ValueInRange"),
521        }
522    }
523
524    #[test]
525    fn test_cycle_graph() {
526        let mut instance = BoundedInstance::new(0, 10);
527        instance.apply_inserts(&[(0, 0, 1), (1, 1, 2), (2, 2, 0)]);
528
529        match instance.query() {
530            InstanceResult::ValueInRange { value, .. } => {
531                assert_eq!(value, 2);
532            }
533            _ => panic!("Expected ValueInRange"),
534        }
535    }
536
537    #[test]
538    fn test_above_range() {
539        let mut instance = BoundedInstance::new(5, 10);
540        instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
541
542        // Min cut is 1, which is below range [5, 10]
543        // Our implementation returns ValueInRange for small cuts anyway
544        match instance.query() {
545            InstanceResult::ValueInRange { value, .. } => {
546                assert_eq!(value, 1);
547            }
548            _ => {}
549        }
550    }
551
552    #[test]
553    fn test_dynamic_updates() {
554        let mut instance = BoundedInstance::new(0, 10);
555
556        instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
557
558        match instance.query() {
559            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
560            _ => panic!("Expected ValueInRange"),
561        }
562
563        // Add edge to form cycle
564        instance.apply_inserts(&[(2, 2, 0)]);
565
566        match instance.query() {
567            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 2),
568            _ => panic!("Expected ValueInRange"),
569        }
570    }
571
572    #[test]
573    fn test_disconnected_graph() {
574        let mut instance = BoundedInstance::new(0, 10);
575        instance.apply_inserts(&[(0, 0, 1), (1, 2, 3)]);
576
577        match instance.query() {
578            InstanceResult::ValueInRange { value, .. } => {
579                assert_eq!(value, 0);
580            }
581            _ => panic!("Expected ValueInRange with value 0"),
582        }
583    }
584
585    #[test]
586    fn test_certificate_tracking() {
587        let mut instance = BoundedInstance::new(0, 10);
588        instance.apply_inserts(&[(0, 0, 1), (1, 1, 2)]);
589
590        let _ = instance.query();
591
592        let cert = instance.certificate();
593        // Certificate should have recorded searches
594        assert!(!cert.localkcut_responses.is_empty() || instance.vertices.len() < 20);
595    }
596}