ruvector_mincut/localkcut/
paper_impl.rs

1//! Paper-Compliant Local K-Cut Implementation
2//!
3//! This module implements the exact API specified in the December 2024 paper
4//! "Deterministic and Exact Fully-dynamic Minimum Cut of Superpolylogarithmic Size"
5//! (arxiv:2512.13105)
6//!
7//! # Key Properties
8//!
9//! - **Deterministic**: No randomness - same input always produces same output
10//! - **Bounded Range**: Searches for cuts with value ≤ budget_k
11//! - **Local Exploration**: BFS-based exploration within bounded radius
12//! - **Witness-Based**: Returns witnesses that certify found cuts
13//!
14//! # Algorithm Overview
15//!
16//! The algorithm performs deterministic BFS from seed vertices:
17//! 1. Start from seed vertices
18//! 2. Expand outward layer by layer (BFS)
19//! 3. Track boundary edges at each layer
20//! 4. If boundary ≤ budget at any layer, create witness
21//! 5. Return smallest cut found or NoneInLocality
22
23use crate::graph::{DynamicGraph, VertexId};
24use crate::instance::WitnessHandle;
25use roaring::RoaringBitmap;
26use std::collections::{HashSet, VecDeque};
27
28/// Query parameters for local k-cut search
29///
30/// Specifies the search parameters for finding a local minimum cut:
31/// - Where to start (seed vertices)
32/// - Maximum cut size to accept (budget)
33/// - How far to search (radius)
34#[derive(Debug, Clone)]
35pub struct LocalKCutQuery {
36    /// Seed vertices defining the search region
37    ///
38    /// The algorithm starts BFS from these vertices. Multiple seeds
39    /// allow searching from different starting points.
40    pub seed_vertices: Vec<VertexId>,
41
42    /// Maximum acceptable cut value
43    ///
44    /// The algorithm only returns cuts with value ≤ budget_k.
45    /// This bounds the search space and ensures polynomial time.
46    pub budget_k: u64,
47
48    /// Maximum search radius (BFS depth)
49    ///
50    /// Limits how far from the seed vertices to explore.
51    /// Larger radius = more thorough search but higher cost.
52    pub radius: usize,
53}
54
55/// Result of a local k-cut search
56///
57/// Either finds a cut within budget or reports that no such cut
58/// exists in the local region around the seed vertices.
59#[derive(Debug, Clone)]
60pub enum LocalKCutResult {
61    /// Found a cut with value ≤ budget_k
62    ///
63    /// The witness certifies the cut and can be used to verify
64    /// correctness or reconstruct the partition.
65    Found {
66        /// Handle to the witness certifying this cut
67        witness: WitnessHandle,
68        /// The actual cut value |δ(U)|
69        cut_value: u64,
70    },
71
72    /// No cut ≤ budget_k found in the local region
73    ///
74    /// This does not mean no such cut exists globally, only that
75    /// none was found within the search radius from the seeds.
76    NoneInLocality,
77}
78
79/// Oracle trait for local k-cut queries
80///
81/// Implementations of this trait can answer local k-cut queries
82/// deterministically. The trait is thread-safe to support parallel
83/// queries across multiple regions.
84pub trait LocalKCutOracle: Send + Sync {
85    /// Search for a local minimum cut satisfying the query
86    ///
87    /// # Arguments
88    ///
89    /// * `graph` - The graph to search in
90    /// * `query` - Query parameters (seeds, budget, radius)
91    ///
92    /// # Returns
93    ///
94    /// Either a witness for a cut ≤ budget_k, or NoneInLocality
95    ///
96    /// # Determinism
97    ///
98    /// For the same graph and query, this method MUST always return
99    /// the same result. No randomness is allowed.
100    fn search(&self, graph: &DynamicGraph, query: LocalKCutQuery) -> LocalKCutResult;
101}
102
103/// Deterministic family generator for seed selection
104///
105/// Generates deterministic families of vertex sets for the derandomized
106/// local k-cut algorithm. Uses vertex ordering to ensure determinism.
107#[derive(Debug, Clone)]
108pub struct DeterministicFamilyGenerator {
109    /// Maximum family size
110    max_size: usize,
111}
112
113impl DeterministicFamilyGenerator {
114    /// Create a new deterministic family generator
115    ///
116    /// # Arguments
117    ///
118    /// * `max_size` - Maximum size of generated families
119    ///
120    /// # Returns
121    ///
122    /// A new generator with deterministic properties
123    pub fn new(max_size: usize) -> Self {
124        Self { max_size }
125    }
126
127    /// Generate deterministic seed vertices from a vertex
128    ///
129    /// Uses the vertex ID and its neighbors to generate a deterministic
130    /// set of seed vertices for exploration.
131    ///
132    /// # Arguments
133    ///
134    /// * `graph` - The graph
135    /// * `v` - Starting vertex
136    ///
137    /// # Returns
138    ///
139    /// A deterministic set of seed vertices including v
140    pub fn generate_seeds(&self, graph: &DynamicGraph, v: VertexId) -> Vec<VertexId> {
141        let mut seeds = vec![v];
142
143        // Deterministically select neighbors based on vertex ID ordering
144        let mut neighbors: Vec<_> = graph.neighbors(v)
145            .into_iter()
146            .map(|(neighbor, _)| neighbor)
147            .collect();
148
149        // Sort for determinism
150        neighbors.sort_unstable();
151
152        // Take up to max_size seeds
153        for &neighbor in neighbors.iter().take(self.max_size.saturating_sub(1)) {
154            seeds.push(neighbor);
155        }
156
157        seeds
158    }
159}
160
161impl Default for DeterministicFamilyGenerator {
162    fn default() -> Self {
163        Self::new(4)
164    }
165}
166
167/// Deterministic Local K-Cut algorithm
168///
169/// Implements the LocalKCutOracle trait using a deterministic BFS-based
170/// exploration strategy. The algorithm:
171///
172/// 1. Starts BFS from seed vertices
173/// 2. Explores outward layer by layer
174/// 3. Tracks boundary size at each layer
175/// 4. Returns the smallest cut found ≤ budget
176///
177/// # Determinism
178///
179/// The algorithm is completely deterministic:
180/// - BFS order determined by vertex ID ordering
181/// - Seed selection based on deterministic family generator
182/// - No random sampling or probabilistic choices
183///
184/// # Time Complexity
185///
186/// O(radius * (|V| + |E|)) for a single query in the worst case,
187/// but typically much faster due to early termination.
188#[derive(Debug, Clone)]
189pub struct DeterministicLocalKCut {
190    /// Maximum radius for local search
191    max_radius: usize,
192
193    /// Deterministic family generator for seed selection
194    #[allow(dead_code)]
195    family_generator: DeterministicFamilyGenerator,
196}
197
198impl DeterministicLocalKCut {
199    /// Create a new deterministic local k-cut oracle
200    ///
201    /// # Arguments
202    ///
203    /// * `max_radius` - Maximum search radius (BFS depth)
204    ///
205    /// # Returns
206    ///
207    /// A new oracle instance
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use ruvector_mincut::localkcut::paper_impl::DeterministicLocalKCut;
213    ///
214    /// let oracle = DeterministicLocalKCut::new(10);
215    /// ```
216    pub fn new(max_radius: usize) -> Self {
217        Self {
218            max_radius,
219            family_generator: DeterministicFamilyGenerator::default(),
220        }
221    }
222
223    /// Create with custom family generator
224    ///
225    /// # Arguments
226    ///
227    /// * `max_radius` - Maximum search radius
228    /// * `family_generator` - Custom family generator
229    ///
230    /// # Returns
231    ///
232    /// A new oracle with custom configuration
233    pub fn with_family_generator(
234        max_radius: usize,
235        family_generator: DeterministicFamilyGenerator,
236    ) -> Self {
237        Self {
238            max_radius,
239            family_generator,
240        }
241    }
242
243    /// Perform deterministic BFS exploration from seeds
244    ///
245    /// Explores the graph layer by layer, tracking the boundary size
246    /// at each step. Returns early if a cut within budget is found.
247    ///
248    /// # Arguments
249    ///
250    /// * `graph` - The graph to explore
251    /// * `seeds` - Starting vertices
252    /// * `budget` - Maximum acceptable boundary size
253    /// * `radius` - Maximum BFS depth
254    ///
255    /// # Returns
256    ///
257    /// Option containing (vertices in cut, boundary size) if found
258    fn deterministic_bfs(
259        &self,
260        graph: &DynamicGraph,
261        seeds: &[VertexId],
262        budget: u64,
263        radius: usize,
264    ) -> Option<(HashSet<VertexId>, u64)> {
265        if seeds.is_empty() {
266            return None;
267        }
268
269        let mut visited = HashSet::new();
270        let mut queue = VecDeque::new();
271        let mut best_cut: Option<(HashSet<VertexId>, u64)> = None;
272
273        // Initialize BFS with seeds
274        for &seed in seeds {
275            if graph.has_vertex(seed) {
276                visited.insert(seed);
277                queue.push_back((seed, 0));
278            }
279        }
280
281        if visited.is_empty() {
282            return None;
283        }
284
285        // Track vertices at each layer for deterministic expansion
286        let mut current_layer = visited.clone();
287
288        // BFS exploration
289        for depth in 0..=radius {
290            // Calculate boundary for current visited set
291            let boundary_size = self.calculate_boundary(graph, &visited);
292
293            // Check if this is a valid cut within budget
294            if boundary_size <= budget && !visited.is_empty() {
295                // Ensure it's a proper partition (not all vertices)
296                if visited.len() < graph.num_vertices() {
297                    // Update best cut if this is better
298                    let should_update = match &best_cut {
299                        None => true,
300                        Some((_, prev_boundary)) => boundary_size < *prev_boundary,
301                    };
302
303                    if should_update {
304                        best_cut = Some((visited.clone(), boundary_size));
305                    }
306                }
307            }
308
309            // Early termination if we found a perfect cut
310            if let Some((_, boundary)) = &best_cut {
311                if *boundary == 0 {
312                    break;
313                }
314            }
315
316            // Don't expand beyond radius
317            if depth >= radius {
318                break;
319            }
320
321            // Expand to next layer deterministically
322            let mut next_layer = HashSet::new();
323            let mut layer_vertices: Vec<_> = current_layer.iter().copied().collect();
324            layer_vertices.sort_unstable(); // Deterministic ordering
325
326            for v in layer_vertices {
327                // Get neighbors and sort for determinism
328                let mut neighbors: Vec<_> = graph.neighbors(v)
329                    .into_iter()
330                    .map(|(neighbor, _)| neighbor)
331                    .filter(|neighbor| !visited.contains(neighbor))
332                    .collect();
333
334                neighbors.sort_unstable();
335
336                for neighbor in neighbors {
337                    if visited.insert(neighbor) {
338                        next_layer.insert(neighbor);
339                        queue.push_back((neighbor, depth + 1));
340                    }
341                }
342            }
343
344            current_layer = next_layer;
345
346            // No more vertices to explore
347            if current_layer.is_empty() {
348                break;
349            }
350        }
351
352        best_cut
353    }
354
355    /// Calculate the boundary size for a vertex set
356    ///
357    /// Counts edges crossing from the vertex set to its complement.
358    ///
359    /// # Arguments
360    ///
361    /// * `graph` - The graph
362    /// * `vertex_set` - Set of vertices on one side
363    ///
364    /// # Returns
365    ///
366    /// Number of edges crossing the cut
367    fn calculate_boundary(&self, graph: &DynamicGraph, vertex_set: &HashSet<VertexId>) -> u64 {
368        let mut boundary_edges = HashSet::new();
369
370        for &v in vertex_set {
371            for (neighbor, edge_id) in graph.neighbors(v) {
372                if !vertex_set.contains(&neighbor) {
373                    // Edge crosses the cut
374                    boundary_edges.insert(edge_id);
375                }
376            }
377        }
378
379        boundary_edges.len() as u64
380    }
381
382    /// Create a witness handle from a cut
383    ///
384    /// # Arguments
385    ///
386    /// * `seed` - Seed vertex in the cut
387    /// * `vertices` - Vertices in the cut set
388    /// * `boundary_size` - Size of the boundary
389    ///
390    /// # Returns
391    ///
392    /// A witness handle certifying the cut
393    fn create_witness(
394        &self,
395        seed: VertexId,
396        vertices: &HashSet<VertexId>,
397        boundary_size: u64,
398    ) -> WitnessHandle {
399        let mut membership = RoaringBitmap::new();
400
401        for &v in vertices {
402            if v <= u32::MAX as u64 {
403                membership.insert(v as u32);
404            }
405        }
406
407        WitnessHandle::new(seed, membership, boundary_size)
408    }
409}
410
411impl LocalKCutOracle for DeterministicLocalKCut {
412    fn search(&self, graph: &DynamicGraph, query: LocalKCutQuery) -> LocalKCutResult {
413        // Validate query parameters
414        if query.seed_vertices.is_empty() {
415            return LocalKCutResult::NoneInLocality;
416        }
417
418        // Use query radius, but cap at max_radius
419        let radius = query.radius.min(self.max_radius);
420
421        // Perform deterministic BFS exploration
422        let result = self.deterministic_bfs(
423            graph,
424            &query.seed_vertices,
425            query.budget_k,
426            radius,
427        );
428
429        match result {
430            Some((vertices, boundary_size)) => {
431                // Pick the first seed that's in the vertex set
432                let seed = query.seed_vertices.iter()
433                    .find(|&&s| vertices.contains(&s))
434                    .copied()
435                    .unwrap_or(query.seed_vertices[0]);
436
437                let witness = self.create_witness(seed, &vertices, boundary_size);
438
439                LocalKCutResult::Found {
440                    witness,
441                    cut_value: boundary_size,
442                }
443            }
444            None => LocalKCutResult::NoneInLocality,
445        }
446    }
447}
448
449impl Default for DeterministicLocalKCut {
450    fn default() -> Self {
451        Self::new(10)
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use std::sync::Arc;
459
460    fn create_simple_graph() -> Arc<DynamicGraph> {
461        let graph = DynamicGraph::new();
462
463        // Create a simple path: 1 - 2 - 3 - 4
464        graph.insert_edge(1, 2, 1.0).unwrap();
465        graph.insert_edge(2, 3, 1.0).unwrap();
466        graph.insert_edge(3, 4, 1.0).unwrap();
467
468        Arc::new(graph)
469    }
470
471    fn create_triangle_graph() -> Arc<DynamicGraph> {
472        let graph = DynamicGraph::new();
473
474        // Triangle: 1 - 2 - 3 - 1
475        graph.insert_edge(1, 2, 1.0).unwrap();
476        graph.insert_edge(2, 3, 1.0).unwrap();
477        graph.insert_edge(3, 1, 1.0).unwrap();
478
479        Arc::new(graph)
480    }
481
482    fn create_dumbbell_graph() -> Arc<DynamicGraph> {
483        let graph = DynamicGraph::new();
484
485        // Two triangles connected by a bridge
486        // Triangle 1: {1, 2, 3}
487        graph.insert_edge(1, 2, 1.0).unwrap();
488        graph.insert_edge(2, 3, 1.0).unwrap();
489        graph.insert_edge(3, 1, 1.0).unwrap();
490
491        // Bridge: 3 - 4
492        graph.insert_edge(3, 4, 1.0).unwrap();
493
494        // Triangle 2: {4, 5, 6}
495        graph.insert_edge(4, 5, 1.0).unwrap();
496        graph.insert_edge(5, 6, 1.0).unwrap();
497        graph.insert_edge(6, 4, 1.0).unwrap();
498
499        Arc::new(graph)
500    }
501
502    #[test]
503    fn test_local_kcut_query_creation() {
504        let query = LocalKCutQuery {
505            seed_vertices: vec![1, 2, 3],
506            budget_k: 10,
507            radius: 5,
508        };
509
510        assert_eq!(query.seed_vertices.len(), 3);
511        assert_eq!(query.budget_k, 10);
512        assert_eq!(query.radius, 5);
513    }
514
515    #[test]
516    fn test_deterministic_family_generator() {
517        let graph = create_simple_graph();
518        let generator = DeterministicFamilyGenerator::new(3);
519
520        let seeds1 = generator.generate_seeds(&graph, 1);
521        let seeds2 = generator.generate_seeds(&graph, 1);
522
523        // Should be deterministic - same input produces same output
524        assert_eq!(seeds1, seeds2);
525
526        // Should include the original vertex
527        assert!(seeds1.contains(&1));
528    }
529
530    #[test]
531    fn test_deterministic_local_kcut_creation() {
532        let oracle = DeterministicLocalKCut::new(10);
533        assert_eq!(oracle.max_radius, 10);
534
535        let default_oracle = DeterministicLocalKCut::default();
536        assert_eq!(default_oracle.max_radius, 10);
537    }
538
539    #[test]
540    fn test_simple_path_cut() {
541        let graph = create_simple_graph();
542        let oracle = DeterministicLocalKCut::new(5);
543
544        let query = LocalKCutQuery {
545            seed_vertices: vec![1],
546            budget_k: 2,
547            radius: 2,
548        };
549
550        let result = oracle.search(&graph, query);
551
552        match result {
553            LocalKCutResult::Found { cut_value, witness } => {
554                assert!(cut_value <= 2);
555                assert!(witness.contains(1));
556                assert_eq!(witness.boundary_size(), cut_value);
557            }
558            LocalKCutResult::NoneInLocality => {
559                // Also acceptable - depends on exploration
560            }
561        }
562    }
563
564    #[test]
565    fn test_triangle_no_cut() {
566        let graph = create_triangle_graph();
567        let oracle = DeterministicLocalKCut::new(5);
568
569        // Triangle has min cut = 2, so budget = 1 should fail
570        let query = LocalKCutQuery {
571            seed_vertices: vec![1],
572            budget_k: 1,
573            radius: 3,
574        };
575
576        let result = oracle.search(&graph, query);
577
578        match result {
579            LocalKCutResult::NoneInLocality => {
580                // Expected - triangle has no cut with value 1
581            }
582            LocalKCutResult::Found { cut_value, .. } => {
583                // If found, must be within budget
584                assert!(cut_value <= 1);
585            }
586        }
587    }
588
589    #[test]
590    fn test_dumbbell_bridge_cut() {
591        let graph = create_dumbbell_graph();
592        let oracle = DeterministicLocalKCut::new(10);
593
594        // Should find the bridge (cut value = 1)
595        let query = LocalKCutQuery {
596            seed_vertices: vec![1],
597            budget_k: 3,
598            radius: 10,
599        };
600
601        let result = oracle.search(&graph, query);
602
603        match result {
604            LocalKCutResult::Found { cut_value, witness } => {
605                // Should find bridge with value 1
606                assert_eq!(cut_value, 1);
607                assert!(witness.contains(1));
608
609                // One triangle should be in the cut
610                let cardinality = witness.cardinality();
611                assert!(cardinality == 3 || cardinality == 4);
612            }
613            LocalKCutResult::NoneInLocality => {
614                panic!("Should find the bridge cut");
615            }
616        }
617    }
618
619    #[test]
620    fn test_determinism() {
621        let graph = create_dumbbell_graph();
622        let oracle = DeterministicLocalKCut::new(10);
623
624        let query = LocalKCutQuery {
625            seed_vertices: vec![1, 2],
626            budget_k: 5,
627            radius: 5,
628        };
629
630        // Run the same query twice
631        let result1 = oracle.search(&graph, query.clone());
632        let result2 = oracle.search(&graph, query);
633
634        // Results should be identical (deterministic)
635        match (result1, result2) {
636            (
637                LocalKCutResult::Found { cut_value: v1, witness: w1 },
638                LocalKCutResult::Found { cut_value: v2, witness: w2 },
639            ) => {
640                assert_eq!(v1, v2);
641                assert_eq!(w1.seed(), w2.seed());
642                assert_eq!(w1.boundary_size(), w2.boundary_size());
643                assert_eq!(w1.cardinality(), w2.cardinality());
644            }
645            (LocalKCutResult::NoneInLocality, LocalKCutResult::NoneInLocality) => {
646                // Both none - deterministic
647            }
648            _ => {
649                panic!("Non-deterministic results!");
650            }
651        }
652    }
653
654    #[test]
655    fn test_empty_seeds() {
656        let graph = create_simple_graph();
657        let oracle = DeterministicLocalKCut::new(5);
658
659        let query = LocalKCutQuery {
660            seed_vertices: vec![],
661            budget_k: 10,
662            radius: 5,
663        };
664
665        let result = oracle.search(&graph, query);
666
667        assert!(matches!(result, LocalKCutResult::NoneInLocality));
668    }
669
670    #[test]
671    fn test_invalid_seed() {
672        let graph = create_simple_graph();
673        let oracle = DeterministicLocalKCut::new(5);
674
675        // Seed vertex doesn't exist in graph
676        let query = LocalKCutQuery {
677            seed_vertices: vec![999],
678            budget_k: 10,
679            radius: 5,
680        };
681
682        let result = oracle.search(&graph, query);
683
684        assert!(matches!(result, LocalKCutResult::NoneInLocality));
685    }
686
687    #[test]
688    fn test_zero_radius() {
689        let graph = create_simple_graph();
690        let oracle = DeterministicLocalKCut::new(5);
691
692        let query = LocalKCutQuery {
693            seed_vertices: vec![1],
694            budget_k: 10,
695            radius: 0,
696        };
697
698        let result = oracle.search(&graph, query);
699
700        // With radius 0, should only consider the seed vertex
701        match result {
702            LocalKCutResult::Found { witness, .. } => {
703                assert_eq!(witness.cardinality(), 1);
704                assert!(witness.contains(1));
705            }
706            LocalKCutResult::NoneInLocality => {
707                // Also acceptable
708            }
709        }
710    }
711
712    #[test]
713    fn test_boundary_calculation() {
714        let graph = create_dumbbell_graph();
715        let oracle = DeterministicLocalKCut::new(5);
716
717        // Triangle vertices {1, 2, 3}
718        let mut vertices = HashSet::new();
719        vertices.insert(1);
720        vertices.insert(2);
721        vertices.insert(3);
722
723        let boundary = oracle.calculate_boundary(&graph, &vertices);
724
725        // Should have exactly 1 boundary edge (the bridge 3-4)
726        assert_eq!(boundary, 1);
727    }
728
729    #[test]
730    fn test_witness_creation() {
731        let oracle = DeterministicLocalKCut::new(5);
732
733        let mut vertices = HashSet::new();
734        vertices.insert(1);
735        vertices.insert(2);
736        vertices.insert(3);
737
738        let witness = oracle.create_witness(1, &vertices, 5);
739
740        assert_eq!(witness.seed(), 1);
741        assert_eq!(witness.boundary_size(), 5);
742        assert_eq!(witness.cardinality(), 3);
743        assert!(witness.contains(1));
744        assert!(witness.contains(2));
745        assert!(witness.contains(3));
746        assert!(!witness.contains(4));
747    }
748
749    #[test]
750    fn test_multiple_seeds() {
751        let graph = create_dumbbell_graph();
752        let oracle = DeterministicLocalKCut::new(10);
753
754        let query = LocalKCutQuery {
755            seed_vertices: vec![1, 2, 3],
756            budget_k: 5,
757            radius: 5,
758        };
759
760        let result = oracle.search(&graph, query);
761
762        match result {
763            LocalKCutResult::Found { witness, .. } => {
764                // Witness should contain at least one of the seeds
765                let contains_seed = witness.contains(1)
766                    || witness.contains(2)
767                    || witness.contains(3);
768                assert!(contains_seed);
769            }
770            LocalKCutResult::NoneInLocality => {
771                // Acceptable
772            }
773        }
774    }
775
776    #[test]
777    fn test_budget_enforcement() {
778        let graph = create_triangle_graph();
779        let oracle = DeterministicLocalKCut::new(5);
780
781        let query = LocalKCutQuery {
782            seed_vertices: vec![1],
783            budget_k: 1,
784            radius: 5,
785        };
786
787        let result = oracle.search(&graph, query);
788
789        // If a cut is found, it MUST respect the budget
790        if let LocalKCutResult::Found { cut_value, .. } = result {
791            assert!(cut_value <= 1);
792        }
793    }
794
795    #[test]
796    fn test_large_radius() {
797        let graph = create_simple_graph();
798        let oracle = DeterministicLocalKCut::new(5);
799
800        // Request radius larger than max_radius
801        let query = LocalKCutQuery {
802            seed_vertices: vec![1],
803            budget_k: 10,
804            radius: 100,
805        };
806
807        // Should not panic, should cap at max_radius
808        let _result = oracle.search(&graph, query);
809    }
810
811    #[test]
812    fn test_witness_properties() {
813        let graph = create_dumbbell_graph();
814        let oracle = DeterministicLocalKCut::new(10);
815
816        let query = LocalKCutQuery {
817            seed_vertices: vec![1],
818            budget_k: 5,
819            radius: 5,
820        };
821
822        if let LocalKCutResult::Found { witness, cut_value } = oracle.search(&graph, query) {
823            // Witness boundary size should match cut value
824            assert_eq!(witness.boundary_size(), cut_value);
825
826            // Witness should be non-empty
827            assert!(witness.cardinality() > 0);
828
829            // Seed should be in witness
830            assert!(witness.contains(witness.seed()));
831        }
832    }
833}