Skip to main content

ruvector_mincut/instance/
stub.rs

1//! Stub implementation of ProperCutInstance
2//!
3//! Brute-force reference implementation for testing.
4//! Recomputes minimum cut on every query - O(2^n) worst case.
5//! Only suitable for small graphs (n < 20).
6
7use super::witness::WitnessHandle;
8use super::{InstanceResult, ProperCutInstance};
9use crate::graph::{DynamicGraph, EdgeId, VertexId};
10use roaring::RoaringBitmap;
11use std::collections::{HashMap, HashSet, VecDeque};
12
13/// Stub instance that does brute-force min cut computation
14///
15/// This implementation:
16/// - Stores a local copy of all edges
17/// - Enumerates all proper subsets on each query
18/// - Checks connectivity via BFS
19/// - Computes exact boundary values
20///
21/// # Performance
22///
23/// - Query: O(2^n ยท m) where n = vertices, m = edges
24/// - Only practical for n < 20
25///
26/// # Purpose
27///
28/// Used as a reference implementation to test the wrapper logic
29/// before the real LocalKCut algorithm is ready.
30pub struct StubInstance {
31    /// Lambda bounds
32    lambda_min: u64,
33    lambda_max: u64,
34    /// Local copy of edges for computation
35    edges: Vec<(VertexId, VertexId, EdgeId)>,
36    /// Vertex set
37    vertices: HashSet<VertexId>,
38    /// Adjacency list: vertex -> [(neighbor, edge_id), ...]
39    adjacency: HashMap<VertexId, Vec<(VertexId, EdgeId)>>,
40}
41
42impl StubInstance {
43    /// Create a new stub instance with initial graph state
44    ///
45    /// This is used for direct testing. The wrapper should use `init()` instead.
46    pub fn new(graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
47        let mut instance = Self {
48            lambda_min,
49            lambda_max,
50            edges: Vec::new(),
51            vertices: HashSet::new(),
52            adjacency: HashMap::new(),
53        };
54
55        // Copy initial graph state
56        for edge in graph.edges() {
57            instance.vertices.insert(edge.source);
58            instance.vertices.insert(edge.target);
59            instance.edges.push((edge.source, edge.target, edge.id));
60        }
61
62        instance.rebuild_adjacency();
63        instance
64    }
65
66    /// Create an empty stub instance for use with the wrapper
67    ///
68    /// The wrapper will apply all edges via apply_inserts/apply_deletes.
69    pub fn new_empty(lambda_min: u64, lambda_max: u64) -> Self {
70        Self {
71            lambda_min,
72            lambda_max,
73            edges: Vec::new(),
74            vertices: HashSet::new(),
75            adjacency: HashMap::new(),
76        }
77    }
78
79    /// Compute minimum cut via brute-force enumeration
80    ///
81    /// For each non-empty proper subset S:
82    /// 1. Check if S is connected
83    /// 2. If connected, compute boundary size
84    /// 3. Track minimum
85    ///
86    /// Returns None if graph is empty or disconnected.
87    fn compute_min_cut(&self) -> Option<(u64, WitnessHandle)> {
88        if self.vertices.is_empty() {
89            return None;
90        }
91
92        let n = self.vertices.len();
93        if n == 1 {
94            // Single vertex: no proper cuts
95            return None;
96        }
97
98        // Stub instance only works for small graphs to avoid overflow
99        // For large graphs, we return a large value to signal AboveRange
100        if n >= 20 {
101            // Return a large value that will trigger AboveRange
102            return None;
103        }
104
105        // Check if graph is connected
106        if !self.is_connected() {
107            // Disconnected graph has min cut 0
108            let membership =
109                RoaringBitmap::from_iter(self.vertices.iter().take(1).map(|&v| v as u32));
110            let seed = *self.vertices.iter().next().unwrap();
111            let witness = WitnessHandle::new(seed, membership, 0);
112            return Some((0, witness));
113        }
114
115        let vertex_vec: Vec<VertexId> = self.vertices.iter().copied().collect();
116        let mut min_cut = u64::MAX;
117        let mut best_set = HashSet::new();
118
119        // Enumerate all non-empty proper subsets (2^n - 2 subsets)
120        // We use bitmasks from 1 to 2^n - 2
121        let max_mask = 1u64 << n;
122
123        for mask in 1..max_mask - 1 {
124            // Build subset from bitmask
125            let mut subset = HashSet::new();
126            for (i, &vertex) in vertex_vec.iter().enumerate() {
127                if mask & (1 << i) != 0 {
128                    subset.insert(vertex);
129                }
130            }
131
132            // Check if subset is connected
133            if !self.is_connected_set(&subset) {
134                continue;
135            }
136
137            // Compute boundary
138            let (boundary_value, _boundary_edges) = self.compute_boundary(&subset);
139
140            if boundary_value < min_cut {
141                min_cut = boundary_value;
142                best_set = subset.clone();
143            }
144        }
145
146        if min_cut == u64::MAX {
147            // No proper connected cuts found (shouldn't happen for connected graphs)
148            return None;
149        }
150
151        // Build witness using new API
152        // Convert HashSet to RoaringBitmap (u32)
153        let membership: RoaringBitmap = best_set.iter().map(|&v| v as u32).collect();
154
155        // Use first vertex in set as seed
156        let seed = *best_set.iter().next().unwrap();
157
158        let witness = WitnessHandle::new(seed, membership, min_cut);
159
160        Some((min_cut, witness))
161    }
162
163    /// Check if a subset of vertices is connected
164    ///
165    /// Uses BFS within the subset to check connectivity.
166    fn is_connected_set(&self, vertices: &HashSet<VertexId>) -> bool {
167        if vertices.is_empty() {
168            return true;
169        }
170
171        // Start BFS from arbitrary vertex in the set
172        let start = *vertices.iter().next().unwrap();
173        let mut visited = HashSet::new();
174        let mut queue = VecDeque::new();
175
176        queue.push_back(start);
177        visited.insert(start);
178
179        while let Some(current) = queue.pop_front() {
180            if let Some(neighbors) = self.adjacency.get(&current) {
181                for &(neighbor, _edge_id) in neighbors {
182                    // Only follow edges within the subset
183                    if vertices.contains(&neighbor) && visited.insert(neighbor) {
184                        queue.push_back(neighbor);
185                    }
186                }
187            }
188        }
189
190        // Connected if we visited all vertices in the subset
191        visited.len() == vertices.len()
192    }
193
194    /// Compute boundary of a vertex set
195    ///
196    /// Returns (boundary_value, boundary_edges).
197    /// Boundary = edges with exactly one endpoint in the set.
198    fn compute_boundary(&self, set: &HashSet<VertexId>) -> (u64, Vec<EdgeId>) {
199        let mut boundary_value = 0u64;
200        let mut boundary_edges = Vec::new();
201
202        for &(u, v, edge_id) in &self.edges {
203            let u_in_set = set.contains(&u);
204            let v_in_set = set.contains(&v);
205
206            // Edge is in boundary if exactly one endpoint is in set
207            if u_in_set != v_in_set {
208                boundary_value += 1;
209                boundary_edges.push(edge_id);
210            }
211        }
212
213        (boundary_value, boundary_edges)
214    }
215
216    /// Check if entire graph is connected
217    fn is_connected(&self) -> bool {
218        self.is_connected_set(&self.vertices)
219    }
220
221    /// Rebuild adjacency list from edges
222    fn rebuild_adjacency(&mut self) {
223        self.adjacency.clear();
224
225        for &(u, v, edge_id) in &self.edges {
226            self.adjacency
227                .entry(u)
228                .or_insert_with(Vec::new)
229                .push((v, edge_id));
230
231            self.adjacency
232                .entry(v)
233                .or_insert_with(Vec::new)
234                .push((u, edge_id));
235        }
236    }
237
238    fn insert(&mut self, edge_id: EdgeId, u: VertexId, v: VertexId) {
239        // Add edge to local copy
240        self.vertices.insert(u);
241        self.vertices.insert(v);
242        self.edges.push((u, v, edge_id));
243        self.rebuild_adjacency();
244    }
245
246    fn delete(&mut self, edge_id: EdgeId, _u: VertexId, _v: VertexId) {
247        // Remove edge from local copy
248        self.edges.retain(|(_, _, eid)| *eid != edge_id);
249        self.rebuild_adjacency();
250    }
251}
252
253impl ProperCutInstance for StubInstance {
254    fn init(_graph: &DynamicGraph, lambda_min: u64, lambda_max: u64) -> Self {
255        // For wrapper use: start empty, wrapper will call apply_inserts
256        Self::new_empty(lambda_min, lambda_max)
257    }
258
259    fn apply_inserts(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
260        for &(edge_id, u, v) in edges {
261            self.insert(edge_id, u, v);
262        }
263    }
264
265    fn apply_deletes(&mut self, edges: &[(EdgeId, VertexId, VertexId)]) {
266        for &(edge_id, u, v) in edges {
267            self.delete(edge_id, u, v);
268        }
269    }
270
271    fn query(&mut self) -> InstanceResult {
272        match self.compute_min_cut() {
273            Some((value, witness)) if value <= self.lambda_max => {
274                InstanceResult::ValueInRange { value, witness }
275            }
276            _ => InstanceResult::AboveRange,
277        }
278    }
279
280    fn bounds(&self) -> (u64, u64) {
281        (self.lambda_min, self.lambda_max)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::graph::DynamicGraph;
289
290    #[test]
291    fn test_empty_graph() {
292        let graph = DynamicGraph::new();
293        let mut instance = StubInstance::new(&graph, 0, 10);
294
295        let result = instance.query();
296        assert!(matches!(result, InstanceResult::AboveRange));
297    }
298
299    #[test]
300    fn test_single_vertex() {
301        let graph = DynamicGraph::new();
302        graph.add_vertex(1);
303
304        let mut instance = StubInstance::new(&graph, 0, 10);
305        let result = instance.query();
306        assert!(matches!(result, InstanceResult::AboveRange));
307    }
308
309    #[test]
310    fn test_path_graph() {
311        // Path: 1 - 2 - 3
312        let graph = DynamicGraph::new();
313        graph.insert_edge(1, 2, 1.0).unwrap();
314        graph.insert_edge(2, 3, 1.0).unwrap();
315
316        let mut instance = StubInstance::new(&graph, 0, 10);
317
318        let result = instance.query();
319        match result {
320            InstanceResult::ValueInRange { value, .. } => {
321                // Min cut of path is 1
322                assert_eq!(value, 1);
323            }
324            _ => panic!("Expected ValueInRange result"),
325        }
326    }
327
328    #[test]
329    fn test_cycle_graph() {
330        // Cycle: 1 - 2 - 3 - 1
331        let graph = DynamicGraph::new();
332        graph.insert_edge(1, 2, 1.0).unwrap();
333        graph.insert_edge(2, 3, 1.0).unwrap();
334        graph.insert_edge(3, 1, 1.0).unwrap();
335
336        let mut instance = StubInstance::new(&graph, 0, 10);
337
338        let result = instance.query();
339        match result {
340            InstanceResult::ValueInRange { value, .. } => {
341                // Min cut of cycle is 2
342                assert_eq!(value, 2);
343            }
344            _ => panic!("Expected ValueInRange result"),
345        }
346    }
347
348    #[test]
349    fn test_complete_graph_k4() {
350        // Complete graph K4
351        let graph = DynamicGraph::new();
352        for i in 1..=4 {
353            for j in i + 1..=4 {
354                graph.insert_edge(i, j, 1.0).unwrap();
355            }
356        }
357
358        let mut instance = StubInstance::new(&graph, 0, 10);
359
360        let result = instance.query();
361        match result {
362            InstanceResult::ValueInRange { value, .. } => {
363                // Min cut of K4 is 3 (minimum degree)
364                assert_eq!(value, 3);
365            }
366            _ => panic!("Expected ValueInRange result"),
367        }
368    }
369
370    #[test]
371    fn test_disconnected_graph() {
372        // Two separate edges: 1-2 and 3-4
373        let graph = DynamicGraph::new();
374        graph.insert_edge(1, 2, 1.0).unwrap();
375        graph.insert_edge(3, 4, 1.0).unwrap();
376
377        let mut instance = StubInstance::new(&graph, 0, 10);
378
379        let result = instance.query();
380        match result {
381            InstanceResult::ValueInRange { value, .. } => {
382                // Disconnected graph has min cut 0
383                assert_eq!(value, 0);
384            }
385            _ => panic!("Expected ValueInRange with value 0"),
386        }
387    }
388
389    #[test]
390    fn test_bridge_graph() {
391        // Two triangles connected by a bridge
392        let graph = DynamicGraph::new();
393        graph.insert_edge(1, 2, 1.0).unwrap();
394        graph.insert_edge(2, 3, 1.0).unwrap();
395        graph.insert_edge(3, 1, 1.0).unwrap();
396        graph.insert_edge(3, 4, 1.0).unwrap(); // Bridge
397        graph.insert_edge(4, 5, 1.0).unwrap();
398        graph.insert_edge(5, 6, 1.0).unwrap();
399        graph.insert_edge(6, 4, 1.0).unwrap();
400
401        let mut instance = StubInstance::new(&graph, 0, 10);
402
403        let result = instance.query();
404        match result {
405            InstanceResult::ValueInRange { value, .. } => {
406                // Min cut is the bridge (value = 1)
407                assert_eq!(value, 1);
408            }
409            _ => panic!("Expected ValueInRange result"),
410        }
411    }
412
413    #[test]
414    fn test_is_connected_set() {
415        let graph = DynamicGraph::new();
416        graph.insert_edge(1, 2, 1.0).unwrap();
417        graph.insert_edge(2, 3, 1.0).unwrap();
418        graph.insert_edge(3, 4, 1.0).unwrap();
419
420        let instance = StubInstance::new(&graph, 0, 10);
421
422        // Test connected subset
423        let mut subset = HashSet::new();
424        subset.insert(1);
425        subset.insert(2);
426        subset.insert(3);
427        assert!(instance.is_connected_set(&subset));
428
429        // Test disconnected subset (1 and 4 not directly connected)
430        let mut subset = HashSet::new();
431        subset.insert(1);
432        subset.insert(4);
433        assert!(!instance.is_connected_set(&subset));
434
435        // Test single vertex (always connected)
436        let mut subset = HashSet::new();
437        subset.insert(1);
438        assert!(instance.is_connected_set(&subset));
439    }
440
441    #[test]
442    fn test_compute_boundary() {
443        let graph = DynamicGraph::new();
444        let e1 = graph.insert_edge(1, 2, 1.0).unwrap();
445        let e2 = graph.insert_edge(2, 3, 1.0).unwrap();
446        let _e3 = graph.insert_edge(3, 4, 1.0).unwrap();
447
448        let instance = StubInstance::new(&graph, 0, 10);
449
450        // Boundary of {1, 2}
451        let mut set = HashSet::new();
452        set.insert(1);
453        set.insert(2);
454        let (value, edges) = instance.compute_boundary(&set);
455        assert_eq!(value, 1); // Only edge 2-3 crosses
456        assert_eq!(edges.len(), 1);
457        assert!(edges.contains(&e2));
458
459        // Boundary of {2}
460        let mut set = HashSet::new();
461        set.insert(2);
462        let (value, edges) = instance.compute_boundary(&set);
463        assert_eq!(value, 2); // Edges 1-2 and 2-3 cross
464        assert_eq!(edges.len(), 2);
465        assert!(edges.contains(&e1));
466        assert!(edges.contains(&e2));
467    }
468
469    #[test]
470    fn test_dynamic_updates() {
471        let graph = DynamicGraph::new();
472        graph.insert_edge(1, 2, 1.0).unwrap();
473        graph.insert_edge(2, 3, 1.0).unwrap();
474
475        let mut instance = StubInstance::new(&graph, 0, 10);
476
477        // Initial min cut (path: 1-2-3) is 1
478        let result = instance.query();
479        match result {
480            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
481            _ => panic!("Expected ValueInRange"),
482        }
483
484        // Insert edge to form cycle
485        let e3_id = 100; // Mock edge ID
486        instance.apply_inserts(&[(e3_id, 3, 1)]);
487
488        // Now min cut (cycle: 1-2-3-1) is 2
489        let result = instance.query();
490        match result {
491            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 2),
492            _ => panic!("Expected ValueInRange"),
493        }
494
495        // Delete one edge to get back to path
496        instance.apply_deletes(&[(e3_id, 3, 1)]);
497
498        // Min cut should be 1 again
499        let result = instance.query();
500        match result {
501            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
502            _ => panic!("Expected ValueInRange"),
503        }
504    }
505
506    #[test]
507    fn test_range_bounds() {
508        let graph = DynamicGraph::new();
509        graph.insert_edge(1, 2, 1.0).unwrap();
510        graph.insert_edge(2, 3, 1.0).unwrap();
511
512        // Instance with range [2, 5]
513        let mut instance = StubInstance::new(&graph, 2, 5);
514
515        // Min cut is 1, which is below range [2,5], but stub only checks <= lambda_max
516        // so it returns ValueInRange
517        let result = instance.query();
518        // Stub doesn't check lambda_min, so behavior depends on implementation
519
520        // Instance with range [0, 1]
521        let mut instance = StubInstance::new(&graph, 0, 1);
522
523        // Min cut is 1, which is in range
524        let result = instance.query();
525        match result {
526            InstanceResult::ValueInRange { value, .. } => assert_eq!(value, 1),
527            _ => panic!("Expected ValueInRange"),
528        }
529
530        // Instance with range [0, 0]
531        let mut instance = StubInstance::new(&graph, 0, 0);
532
533        // Min cut is 1, which is above range
534        let result = instance.query();
535        assert!(matches!(result, InstanceResult::AboveRange));
536    }
537
538    #[test]
539    fn test_witness_information() {
540        let graph = DynamicGraph::new();
541        graph.insert_edge(1, 2, 1.0).unwrap();
542        graph.insert_edge(2, 3, 1.0).unwrap();
543
544        let mut instance = StubInstance::new(&graph, 0, 10);
545
546        let result = instance.query();
547        match result {
548            InstanceResult::ValueInRange { value, witness } => {
549                assert_eq!(value, 1);
550                assert_eq!(witness.boundary_size(), 1);
551                assert!(witness.cardinality() > 0);
552                assert!(witness.cardinality() < 3); // Proper cut
553            }
554            _ => panic!("Expected ValueInRange with witness"),
555        }
556    }
557}