Skip to main content

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