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