ruvector_mincut/sparsify/
mod.rs

1//! Graph Sparsification for Approximate Minimum Cuts
2//!
3//! Implements sparsification that preserves (1±ε) approximation of all cuts
4//! using O(n log n / ε²) edges.
5//!
6//! This module provides two main sparsification approaches:
7//! 1. **Benczúr-Karger**: Randomized sparsification based on edge strengths
8//! 2. **Nagamochi-Ibaraki**: Deterministic sparsification using connectivity certificates
9//!
10//! # Example
11//!
12//! ```rust
13//! use ruvector_mincut::graph::DynamicGraph;
14//! use ruvector_mincut::sparsify::{SparsifyConfig, SparseGraph};
15//!
16//! let graph = DynamicGraph::new();
17//! graph.insert_edge(1, 2, 1.0).unwrap();
18//! graph.insert_edge(2, 3, 1.0).unwrap();
19//! graph.insert_edge(3, 4, 1.0).unwrap();
20//! graph.insert_edge(4, 1, 1.0).unwrap();
21//! graph.insert_edge(1, 3, 1.0).unwrap();
22//!
23//! let config = SparsifyConfig {
24//!     epsilon: 0.1,
25//!     seed: Some(42),
26//!     max_edges: None,
27//! };
28//!
29//! let sparse = SparseGraph::from_graph(&graph, config).unwrap();
30//! assert!(sparse.num_edges() <= graph.num_edges());
31//! ```
32
33use std::collections::{HashMap, HashSet};
34use std::sync::Arc;
35use rand::prelude::*;
36use rand::rngs::StdRng;
37use crate::graph::{DynamicGraph, VertexId, EdgeId, Weight};
38use crate::error::{MinCutError, Result};
39
40/// Configuration for sparsification
41#[derive(Debug, Clone)]
42pub struct SparsifyConfig {
43    /// Approximation parameter (0 < ε ≤ 1)
44    pub epsilon: f64,
45    /// Random seed for reproducibility
46    pub seed: Option<u64>,
47    /// Maximum number of edges in sparse graph
48    pub max_edges: Option<usize>,
49}
50
51impl Default for SparsifyConfig {
52    fn default() -> Self {
53        Self {
54            epsilon: 0.1,
55            seed: None,
56            max_edges: None,
57        }
58    }
59}
60
61impl SparsifyConfig {
62    /// Create a new configuration
63    pub fn new(epsilon: f64) -> Result<Self> {
64        if epsilon <= 0.0 || epsilon > 1.0 {
65            return Err(MinCutError::InvalidEpsilon(epsilon));
66        }
67        Ok(Self {
68            epsilon,
69            seed: None,
70            max_edges: None,
71        })
72    }
73
74    /// Set random seed
75    pub fn with_seed(mut self, seed: u64) -> Self {
76        self.seed = Some(seed);
77        self
78    }
79
80    /// Set maximum number of edges
81    pub fn with_max_edges(mut self, max_edges: usize) -> Self {
82        self.max_edges = Some(max_edges);
83        self
84    }
85}
86
87/// A sparsified graph that preserves cut structure
88pub struct SparseGraph {
89    /// The sparse graph
90    graph: DynamicGraph,
91    /// Original edge to scaled weight mapping
92    edge_weights: HashMap<EdgeId, Weight>,
93    /// Epsilon used for this sparsification
94    epsilon: f64,
95    /// Number of edges in original graph
96    original_edges: usize,
97    /// Random number generator
98    rng: StdRng,
99    /// Edge strength calculator
100    strength_calc: EdgeStrength,
101}
102
103impl SparseGraph {
104    /// Create a sparsified version of the graph
105    pub fn from_graph(graph: &DynamicGraph, config: SparsifyConfig) -> Result<Self> {
106        if config.epsilon <= 0.0 || config.epsilon > 1.0 {
107            return Err(MinCutError::InvalidEpsilon(config.epsilon));
108        }
109
110        let original_edges = graph.num_edges();
111        let n = graph.num_vertices();
112
113        if n == 0 {
114            return Err(MinCutError::EmptyGraph);
115        }
116
117        // Initialize RNG
118        let rng = match config.seed {
119            Some(seed) => StdRng::seed_from_u64(seed),
120            None => StdRng::from_entropy(),
121        };
122
123        // Create sparse graph
124        let sparse_graph = DynamicGraph::with_capacity(n, original_edges);
125
126        // Add all vertices
127        for v in graph.vertices() {
128            sparse_graph.add_vertex(v);
129        }
130
131        let mut edge_weights = HashMap::new();
132        let mut strength_calc = EdgeStrength::new(Arc::new(graph.clone()));
133
134        // Use Benczúr-Karger sparsification
135        Self::benczur_karger_sparsify(
136            graph,
137            &sparse_graph,
138            &mut edge_weights,
139            &mut strength_calc,
140            config.epsilon,
141            &mut StdRng::seed_from_u64(config.seed.unwrap_or(42)),
142            config.max_edges,
143        )?;
144
145        Ok(Self {
146            graph: sparse_graph,
147            edge_weights,
148            epsilon: config.epsilon,
149            original_edges,
150            rng,
151            strength_calc,
152        })
153    }
154
155    /// Benczúr-Karger sparsification algorithm
156    fn benczur_karger_sparsify(
157        original: &DynamicGraph,
158        sparse: &DynamicGraph,
159        edge_weights: &mut HashMap<EdgeId, Weight>,
160        strength_calc: &mut EdgeStrength,
161        epsilon: f64,
162        rng: &mut StdRng,
163        max_edges: Option<usize>,
164    ) -> Result<()> {
165        let n = original.num_vertices() as f64;
166        let c = 6.0; // Constant for sampling probability
167
168        // Compute edge strengths
169        let strengths = strength_calc.compute_all();
170
171        let edges = original.edges();
172        let mut edges_added = 0;
173        let target_edges = max_edges.unwrap_or(usize::MAX);
174
175        for edge in edges {
176            // Get edge strength (use weight as lower bound if not computed)
177            let strength = strengths.get(&edge.id).copied().unwrap_or(edge.weight);
178
179            // Compute sampling probability
180            let prob = sample_probability(strength, epsilon, n, c);
181
182            // Sample edge with probability prob
183            if rng.gen::<f64>() < prob && edges_added < target_edges {
184                // Scale weight by 1/prob
185                let scaled_weight = edge.weight / prob;
186
187                // Add to sparse graph
188                if let Ok(new_edge_id) = sparse.insert_edge(
189                    edge.source,
190                    edge.target,
191                    scaled_weight
192                ) {
193                    edge_weights.insert(new_edge_id, edge.weight);
194                    edges_added += 1;
195                }
196            }
197        }
198
199        Ok(())
200    }
201
202    /// Get the sparse graph
203    pub fn graph(&self) -> &DynamicGraph {
204        &self.graph
205    }
206
207    /// Get the number of edges (should be O(n log n / ε²))
208    pub fn num_edges(&self) -> usize {
209        self.graph.num_edges()
210    }
211
212    /// Get the sparsification ratio
213    pub fn sparsification_ratio(&self) -> f64 {
214        if self.original_edges == 0 {
215            return 1.0;
216        }
217        self.num_edges() as f64 / self.original_edges as f64
218    }
219
220    /// Query approximate minimum cut on sparse graph
221    pub fn approximate_min_cut(&self) -> f64 {
222        // Simple approximation using minimum degree
223        if self.graph.num_vertices() == 0 {
224            return 0.0;
225        }
226
227        let vertices = self.graph.vertices();
228        let mut min_cut = f64::INFINITY;
229
230        for v in vertices {
231            let mut cut_weight = 0.0;
232            for (neighbor, _edge_id) in self.graph.neighbors(v) {
233                if let Some(edge) = self.graph.get_edge(v, neighbor) {
234                    cut_weight += edge.weight;
235                }
236            }
237            min_cut = min_cut.min(cut_weight);
238        }
239
240        min_cut
241    }
242
243    /// Update for edge insertion in original graph
244    pub fn insert_edge(&mut self, u: VertexId, v: VertexId, weight: Weight) -> Result<()> {
245        // Invalidate strength cache
246        self.strength_calc.invalidate(u);
247        self.strength_calc.invalidate(v);
248
249        // Compute strength for new edge
250        let strength = self.strength_calc.compute(u, v);
251
252        // Compute sampling probability
253        let n = self.graph.num_vertices() as f64;
254        let c = 6.0;
255        let prob = sample_probability(strength, self.epsilon, n, c);
256
257        // Sample edge
258        if self.rng.gen::<f64>() < prob {
259            let scaled_weight = weight / prob;
260            if let Ok(new_edge_id) = self.graph.insert_edge(u, v, scaled_weight) {
261                self.edge_weights.insert(new_edge_id, weight);
262            }
263        }
264
265        Ok(())
266    }
267
268    /// Update for edge deletion in original graph
269    pub fn delete_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
270        // Invalidate strength cache
271        self.strength_calc.invalidate(u);
272        self.strength_calc.invalidate(v);
273
274        // Try to delete from sparse graph (may not exist if not sampled)
275        let _ = self.graph.delete_edge(u, v);
276
277        Ok(())
278    }
279
280    /// Get epsilon value
281    pub fn epsilon(&self) -> f64 {
282        self.epsilon
283    }
284}
285
286/// Edge strength calculator for sparsification sampling
287pub struct EdgeStrength {
288    /// Graph reference
289    graph: Arc<DynamicGraph>,
290    /// Cached strengths
291    strengths: HashMap<EdgeId, f64>,
292}
293
294impl EdgeStrength {
295    /// Create new strength calculator
296    pub fn new(graph: Arc<DynamicGraph>) -> Self {
297        Self {
298            graph,
299            strengths: HashMap::new(),
300        }
301    }
302
303    /// Compute strength of edge (u, v)
304    /// Strength = max-flow between u and v in graph without edge (u,v)
305    /// For efficiency, approximate using connectivity
306    pub fn compute(&mut self, u: VertexId, v: VertexId) -> f64 {
307        // Get edge if it exists
308        let edge = self.graph.get_edge(u, v);
309        let edge_id = edge.map(|e| e.id);
310
311        // Check cache
312        if let Some(&strength) = edge_id.and_then(|id| self.strengths.get(&id)) {
313            return strength;
314        }
315
316        // Approximate strength using local connectivity
317        // Better approximation: sum of edge weights incident to u and v
318        let weight_u: f64 = self.graph.neighbors(u)
319            .iter()
320            .filter_map(|(neighbor, _)| self.graph.edge_weight(u, *neighbor))
321            .sum();
322
323        let weight_v: f64 = self.graph.neighbors(v)
324            .iter()
325            .filter_map(|(neighbor, _)| self.graph.edge_weight(v, *neighbor))
326            .sum();
327
328        let strength = weight_u.min(weight_v).max(1.0);
329
330        // Cache if we have edge_id
331        if let Some(id) = edge_id {
332            self.strengths.insert(id, strength);
333        }
334
335        strength
336    }
337
338    /// Compute all edge strengths
339    pub fn compute_all(&mut self) -> HashMap<EdgeId, f64> {
340        let edges = self.graph.edges();
341
342        for edge in edges {
343            if !self.strengths.contains_key(&edge.id) {
344                let strength = self.compute(edge.source, edge.target);
345                self.strengths.insert(edge.id, strength);
346            }
347        }
348
349        self.strengths.clone()
350    }
351
352    /// Invalidate cached strengths for edges incident to vertex
353    pub fn invalidate(&mut self, v: VertexId) {
354        let neighbors = self.graph.neighbors(v);
355        for (_neighbor, edge_id) in neighbors {
356            self.strengths.remove(&edge_id);
357        }
358    }
359}
360
361/// Nagamochi-Ibaraki sparsification (deterministic)
362pub struct NagamochiIbaraki {
363    /// The graph
364    graph: Arc<DynamicGraph>,
365}
366
367impl NagamochiIbaraki {
368    /// Create new NI sparsifier
369    pub fn new(graph: Arc<DynamicGraph>) -> Self {
370        Self { graph }
371    }
372
373    /// Compute a sparse certificate preserving minimum cuts up to k
374    pub fn sparse_k_certificate(&self, k: usize) -> Result<DynamicGraph> {
375        let n = self.graph.num_vertices();
376        if n == 0 {
377            return Err(MinCutError::EmptyGraph);
378        }
379
380        // Compute minimum degree ordering
381        let order = self.min_degree_ordering();
382
383        // Scan connectivity
384        let connectivity = self.scan_connectivity(&order);
385
386        // Build sparse certificate
387        let sparse = DynamicGraph::with_capacity(n, k * n);
388
389        // Add all vertices
390        for v in self.graph.vertices() {
391            sparse.add_vertex(v);
392        }
393
394        // Add edges with connectivity >= k
395        for edge in self.graph.edges() {
396            if let Some(&conn) = connectivity.get(&edge.id) {
397                if conn >= k {
398                    let _ = sparse.insert_edge(edge.source, edge.target, edge.weight);
399                }
400            }
401        }
402
403        Ok(sparse)
404    }
405
406    /// Compute minimum degree ordering
407    fn min_degree_ordering(&self) -> Vec<VertexId> {
408        let mut remaining: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
409        let mut order = Vec::with_capacity(remaining.len());
410
411        // Track degrees
412        let mut degrees: HashMap<VertexId, usize> = self.graph.vertices()
413            .iter()
414            .map(|&v| (v, self.graph.degree(v)))
415            .collect();
416
417        while !remaining.is_empty() {
418            // Find vertex with minimum degree among remaining
419            let (&min_v, _) = degrees.iter()
420                .filter(|(v, _)| remaining.contains(v))
421                .min_by_key(|(_, &deg)| deg)
422                .unwrap();
423
424            order.push(min_v);
425            remaining.remove(&min_v);
426
427            // Update degrees of neighbors
428            for (neighbor, _) in self.graph.neighbors(min_v) {
429                if remaining.contains(&neighbor) {
430                    if let Some(deg) = degrees.get_mut(&neighbor) {
431                        *deg = deg.saturating_sub(1);
432                    }
433                }
434            }
435        }
436
437        order
438    }
439
440    /// Scan vertices to compute edge connectivity
441    fn scan_connectivity(&self, order: &[VertexId]) -> HashMap<EdgeId, usize> {
442        let mut connectivity = HashMap::new();
443        let mut scanned = HashSet::new();
444
445        for &v in order.iter().rev() {
446            scanned.insert(v);
447
448            // For each edge from v to scanned vertices
449            for (neighbor, edge_id) in self.graph.neighbors(v) {
450                if scanned.contains(&neighbor) {
451                    // Connectivity is the number of scanned neighbors
452                    let conn = scanned.len();
453                    connectivity.insert(edge_id, conn);
454                }
455            }
456        }
457
458        connectivity
459    }
460}
461
462/// Karger's random sampling sparsification
463pub fn karger_sparsify(
464    graph: &DynamicGraph,
465    epsilon: f64,
466    seed: Option<u64>,
467) -> Result<SparseGraph> {
468    let config = SparsifyConfig::new(epsilon)?
469        .with_seed(seed.unwrap_or(42));
470
471    SparseGraph::from_graph(graph, config)
472}
473
474/// Compute sampling probability for an edge based on its strength
475fn sample_probability(strength: f64, epsilon: f64, n: f64, c: f64) -> f64 {
476    if strength <= 0.0 {
477        return 0.0;
478    }
479
480    // p_e = min(1, c * log(n) / (ε² * λ_e))
481    let numerator = c * n.ln();
482    let denominator = epsilon * epsilon * strength;
483
484    (numerator / denominator).min(1.0)
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490
491    fn create_triangle_graph() -> DynamicGraph {
492        let g = DynamicGraph::new();
493        g.insert_edge(1, 2, 1.0).unwrap();
494        g.insert_edge(2, 3, 1.0).unwrap();
495        g.insert_edge(3, 1, 1.0).unwrap();
496        g
497    }
498
499    fn create_complete_graph(n: usize) -> DynamicGraph {
500        let g = DynamicGraph::new();
501        for i in 0..n {
502            for j in (i + 1)..n {
503                g.insert_edge(i as u64, j as u64, 1.0).unwrap();
504            }
505        }
506        g
507    }
508
509    fn create_path_graph(n: usize) -> DynamicGraph {
510        let g = DynamicGraph::new();
511        for i in 0..(n - 1) {
512            g.insert_edge(i as u64, (i + 1) as u64, 1.0).unwrap();
513        }
514        g
515    }
516
517    #[test]
518    fn test_sparsify_config_default() {
519        let config = SparsifyConfig::default();
520        assert_eq!(config.epsilon, 0.1);
521        assert_eq!(config.seed, None);
522        assert_eq!(config.max_edges, None);
523    }
524
525    #[test]
526    fn test_sparsify_config_new() {
527        let config = SparsifyConfig::new(0.2).unwrap();
528        assert_eq!(config.epsilon, 0.2);
529    }
530
531    #[test]
532    fn test_sparsify_config_invalid_epsilon() {
533        assert!(SparsifyConfig::new(0.0).is_err());
534        assert!(SparsifyConfig::new(-0.1).is_err());
535        assert!(SparsifyConfig::new(1.5).is_err());
536    }
537
538    #[test]
539    fn test_sparsify_config_builder() {
540        let config = SparsifyConfig::new(0.1).unwrap()
541            .with_seed(42)
542            .with_max_edges(10);
543
544        assert_eq!(config.epsilon, 0.1);
545        assert_eq!(config.seed, Some(42));
546        assert_eq!(config.max_edges, Some(10));
547    }
548
549    #[test]
550    fn test_sparse_graph_triangle() {
551        let g = create_triangle_graph();
552        let config = SparsifyConfig::new(0.1).unwrap().with_seed(42);
553
554        let sparse = SparseGraph::from_graph(&g, config).unwrap();
555
556        assert!(sparse.num_edges() <= g.num_edges());
557        assert_eq!(sparse.epsilon(), 0.1);
558        assert_eq!(sparse.graph().num_vertices(), g.num_vertices());
559    }
560
561    #[test]
562    fn test_sparse_graph_sparsification_ratio() {
563        let g = create_complete_graph(10);
564        let config = SparsifyConfig::new(0.2).unwrap().with_seed(123);
565
566        let sparse = SparseGraph::from_graph(&g, config).unwrap();
567        let ratio = sparse.sparsification_ratio();
568
569        assert!(ratio >= 0.0 && ratio <= 1.0);
570        println!("Sparsification ratio: {}", ratio);
571    }
572
573    #[test]
574    fn test_sparse_graph_max_edges() {
575        let g = create_complete_graph(10);
576        let config = SparsifyConfig::new(0.1).unwrap()
577            .with_seed(42)
578            .with_max_edges(20);
579
580        let sparse = SparseGraph::from_graph(&g, config).unwrap();
581
582        assert!(sparse.num_edges() <= 20);
583    }
584
585    #[test]
586    fn test_sparse_graph_empty_graph() {
587        let g = DynamicGraph::new();
588        let config = SparsifyConfig::default();
589
590        let result = SparseGraph::from_graph(&g, config);
591        assert!(result.is_err());
592    }
593
594    #[test]
595    fn test_sparse_graph_approximate_min_cut() {
596        let g = create_path_graph(5);
597        let config = SparsifyConfig::new(0.1).unwrap().with_seed(42);
598
599        let sparse = SparseGraph::from_graph(&g, config).unwrap();
600        let min_cut = sparse.approximate_min_cut();
601
602        // Path graph has min cut of approximately 1.0
603        assert!(min_cut >= 0.0);
604    }
605
606    #[test]
607    fn test_sparse_graph_insert_edge() {
608        let g = create_triangle_graph();
609        let config = SparsifyConfig::new(0.1).unwrap().with_seed(42);
610
611        let mut sparse = SparseGraph::from_graph(&g, config).unwrap();
612        let initial_edges = sparse.num_edges();
613
614        sparse.insert_edge(4, 5, 1.0).unwrap();
615
616        // May or may not add edge due to sampling
617        assert!(sparse.num_edges() >= initial_edges);
618    }
619
620    #[test]
621    fn test_sparse_graph_delete_edge() {
622        let g = create_triangle_graph();
623        let config = SparsifyConfig::new(0.5).unwrap().with_seed(42);
624
625        let mut sparse = SparseGraph::from_graph(&g, config).unwrap();
626
627        // Try to delete an edge
628        let result = sparse.delete_edge(1, 2);
629        assert!(result.is_ok());
630    }
631
632    #[test]
633    fn test_edge_strength_compute() {
634        let g = create_triangle_graph();
635        let mut strength_calc = EdgeStrength::new(Arc::new(g));
636
637        let strength = strength_calc.compute(1, 2);
638        assert!(strength > 0.0);
639    }
640
641    #[test]
642    fn test_edge_strength_compute_all() {
643        let g = create_complete_graph(5);
644        let mut strength_calc = EdgeStrength::new(Arc::new(g));
645
646        let strengths = strength_calc.compute_all();
647        assert!(!strengths.is_empty());
648
649        for (_, strength) in strengths {
650            assert!(strength > 0.0);
651        }
652    }
653
654    #[test]
655    fn test_edge_strength_invalidate() {
656        let g = create_triangle_graph();
657        let mut strength_calc = EdgeStrength::new(Arc::new(g));
658
659        // Compute all strengths
660        strength_calc.compute_all();
661        let initial_count = strength_calc.strengths.len();
662
663        // Invalidate vertex 1
664        strength_calc.invalidate(1);
665
666        // Should have fewer cached strengths
667        assert!(strength_calc.strengths.len() < initial_count);
668    }
669
670    #[test]
671    fn test_nagamochi_ibaraki_min_degree_ordering() {
672        let g = create_path_graph(5);
673        let ni = NagamochiIbaraki::new(Arc::new(g));
674
675        let order = ni.min_degree_ordering();
676        assert_eq!(order.len(), 5);
677
678        // All vertices should be in the ordering
679        let order_set: HashSet<_> = order.iter().copied().collect();
680        assert_eq!(order_set.len(), 5);
681    }
682
683    #[test]
684    fn test_nagamochi_ibaraki_sparse_certificate() {
685        let g = create_complete_graph(6);
686        let ni = NagamochiIbaraki::new(Arc::new(g.clone()));
687
688        let sparse = ni.sparse_k_certificate(3).unwrap();
689
690        // Should preserve high-connectivity edges
691        assert!(sparse.num_edges() <= g.num_edges());
692        assert_eq!(sparse.num_vertices(), g.num_vertices());
693    }
694
695    #[test]
696    fn test_nagamochi_ibaraki_empty_graph() {
697        let g = DynamicGraph::new();
698        let ni = NagamochiIbaraki::new(Arc::new(g));
699
700        let result = ni.sparse_k_certificate(2);
701        assert!(result.is_err());
702    }
703
704    #[test]
705    fn test_karger_sparsify() {
706        let g = create_complete_graph(8);
707
708        let sparse = karger_sparsify(&g, 0.2, Some(42)).unwrap();
709
710        assert!(sparse.num_edges() <= g.num_edges());
711        assert_eq!(sparse.epsilon(), 0.2);
712    }
713
714    #[test]
715    fn test_karger_sparsify_invalid_epsilon() {
716        let g = create_triangle_graph();
717
718        let result = karger_sparsify(&g, 0.0, Some(42));
719        assert!(result.is_err());
720    }
721
722    #[test]
723    fn test_sample_probability() {
724        let n = 100.0;
725        let epsilon = 0.1;
726        let c = 6.0;
727
728        // High strength -> low probability
729        let prob_high = sample_probability(100.0, epsilon, n, c);
730
731        // Low strength -> high probability (capped at 1.0)
732        let prob_low = sample_probability(1.0, epsilon, n, c);
733
734        assert!(prob_high <= prob_low);
735        assert!(prob_high >= 0.0 && prob_high <= 1.0);
736        assert!(prob_low >= 0.0 && prob_low <= 1.0);
737    }
738
739    #[test]
740    fn test_sample_probability_zero_strength() {
741        let prob = sample_probability(0.0, 0.1, 100.0, 6.0);
742        assert_eq!(prob, 0.0);
743    }
744
745    #[test]
746    fn test_sample_probability_always_capped() {
747        let prob = sample_probability(0.001, 0.1, 100.0, 100.0);
748        assert!(prob <= 1.0);
749    }
750
751    #[test]
752    fn test_sparsification_preserves_vertices() {
753        let g = create_complete_graph(10);
754        let original_vertices: HashSet<_> = g.vertices().into_iter().collect();
755
756        let config = SparsifyConfig::new(0.15).unwrap().with_seed(999);
757        let sparse = SparseGraph::from_graph(&g, config).unwrap();
758
759        let sparse_vertices: HashSet<_> = sparse.graph().vertices().into_iter().collect();
760
761        // All vertices should be preserved
762        assert_eq!(original_vertices, sparse_vertices);
763    }
764
765    #[test]
766    fn test_sparsification_weighted_graph() {
767        let g = DynamicGraph::new();
768        g.insert_edge(1, 2, 2.0).unwrap();
769        g.insert_edge(2, 3, 3.0).unwrap();
770        g.insert_edge(3, 4, 4.0).unwrap();
771        g.insert_edge(4, 1, 5.0).unwrap();
772
773        let config = SparsifyConfig::new(0.3).unwrap().with_seed(777);
774        let sparse = SparseGraph::from_graph(&g, config).unwrap();
775
776        // Should handle weighted edges correctly
777        assert!(sparse.num_edges() <= g.num_edges());
778    }
779
780    #[test]
781    fn test_deterministic_with_seed() {
782        let g = create_complete_graph(8);
783
784        let sparse1 = karger_sparsify(&g, 0.2, Some(12345)).unwrap();
785        let sparse2 = karger_sparsify(&g, 0.2, Some(12345)).unwrap();
786
787        // Same seed should produce same number of edges
788        assert_eq!(sparse1.num_edges(), sparse2.num_edges());
789    }
790
791    #[test]
792    fn test_edge_strength_caching() {
793        let g = create_complete_graph(6);
794        let mut strength_calc = EdgeStrength::new(Arc::new(g));
795
796        // First computation
797        let strength1 = strength_calc.compute(0, 1);
798
799        // Second computation should use cache
800        let strength2 = strength_calc.compute(0, 1);
801
802        assert_eq!(strength1, strength2);
803    }
804
805    #[test]
806    fn test_nagamochi_ibaraki_scan_connectivity() {
807        let g = create_complete_graph(5);
808        let ni = NagamochiIbaraki::new(Arc::new(g.clone()));
809
810        let order = ni.min_degree_ordering();
811        let connectivity = ni.scan_connectivity(&order);
812
813        // Complete graph should have high connectivity
814        assert!(!connectivity.is_empty());
815
816        for (_, conn) in connectivity {
817            assert!(conn > 0);
818        }
819    }
820
821    #[test]
822    fn test_sparse_graph_ratio_bounds() {
823        let g = create_complete_graph(10);
824
825        // Very strict epsilon -> more edges kept
826        let config_strict = SparsifyConfig::new(0.01).unwrap().with_seed(42);
827        let sparse_strict = SparseGraph::from_graph(&g, config_strict).unwrap();
828
829        // Loose epsilon -> fewer edges kept
830        let config_loose = SparsifyConfig::new(0.5).unwrap().with_seed(42);
831        let sparse_loose = SparseGraph::from_graph(&g, config_loose).unwrap();
832
833        // Stricter epsilon should generally keep more edges
834        // (though this is probabilistic and may not always hold)
835        assert!(sparse_strict.sparsification_ratio() >= 0.0);
836        assert!(sparse_loose.sparsification_ratio() >= 0.0);
837    }
838}