ruvector_mincut/expander/
mod.rs

1//! Expander Decomposition for Subpolynomial Dynamic Min-Cut
2//!
3//! Implements deterministic expander decomposition following:
4//! - Chuzhoy et al. "Deterministic Algorithms for Decremental Approximate Shortest Paths"
5//! - Saranurak-Wang "Expander Decomposition and Pruning"
6//!
7//! Key property: Decomposes graph into φ-expanders where φ = 1/(log^{O(1)} n)
8//!
9//! # Overview
10//!
11//! An expander decomposition partitions a graph into components where each component
12//! has high expansion (conductance). This is critical for achieving subpolynomial
13//! update time because:
14//!
15//! 1. Updates within an expander can be handled efficiently
16//! 2. The decomposition has O(log n) levels
17//! 3. Only O(log n / log log n) levels are affected per update
18//!
19//! # Algorithm
20//!
21//! The decomposition uses a recursive trimming approach:
22//!
23//! 1. **Trimming**: Identify and remove low-conductance components
24//! 2. **Recursive Partition**: Split remaining graph and recurse
25//! 3. **Hierarchical Structure**: Build O(log n) level hierarchy
26//! 4. **Dynamic Updates**: Maintain decomposition under edge insertions/deletions
27//!
28//! # Complexity
29//!
30//! - Build: O(m log n) where m = edges, n = vertices
31//! - Conductance computation: O(m)
32//! - Update: O(log n) levels affected, O(m') work per level
33//!
34//! # Example
35//!
36//! ```rust
37//! use std::sync::Arc;
38//! use ruvector_mincut::graph::DynamicGraph;
39//! use ruvector_mincut::expander::ExpanderDecomposition;
40//!
41//! // Create a graph
42//! let graph = Arc::new(DynamicGraph::new());
43//! graph.insert_edge(1, 2, 1.0).unwrap();
44//! graph.insert_edge(2, 3, 1.0).unwrap();
45//! graph.insert_edge(3, 1, 1.0).unwrap();
46//!
47//! // Build expander decomposition with conductance threshold
48//! let phi = 0.1; // Conductance threshold
49//! let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
50//!
51//! // Query decomposition
52//! println!("Number of levels: {}", decomp.num_levels());
53//!
54//! // Handle dynamic updates
55//! decomp.insert_edge(1, 4).unwrap();
56//! decomp.delete_edge(2, 3).unwrap();
57//! ```
58
59use crate::graph::{DynamicGraph, VertexId, EdgeId, Weight};
60use crate::error::{MinCutError, Result};
61use std::collections::{HashMap, HashSet, VecDeque};
62use std::sync::Arc;
63
64/// Expansion (conductance) threshold
65///
66/// φ(S) = cut(S, V\S) / min(vol(S), vol(V\S))
67/// where vol(S) is the sum of degrees of vertices in S
68pub type Conductance = f64;
69
70/// A component in the expander decomposition
71///
72/// Each component represents a high-expansion subgraph
73#[derive(Debug, Clone)]
74pub struct ExpanderComponent {
75    /// Component ID
76    pub id: usize,
77    /// Vertices in this component
78    pub vertices: HashSet<VertexId>,
79    /// Conductance of this component
80    pub conductance: Conductance,
81    /// Level in the hierarchy (0 = finest, higher = coarser)
82    pub level: usize,
83    /// Inter-component edges (boundary edges)
84    pub boundary_edges: Vec<EdgeId>,
85    /// Volume (sum of degrees) of this component
86    pub volume: f64,
87}
88
89impl ExpanderComponent {
90    /// Create a new expander component
91    fn new(id: usize, vertices: HashSet<VertexId>, level: usize) -> Self {
92        Self {
93            id,
94            vertices,
95            conductance: 0.0,
96            level,
97            boundary_edges: Vec::new(),
98            volume: 0.0,
99        }
100    }
101
102    /// Check if this component contains a vertex
103    pub fn contains(&self, v: VertexId) -> bool {
104        self.vertices.contains(&v)
105    }
106
107    /// Get the size of this component
108    pub fn size(&self) -> usize {
109        self.vertices.len()
110    }
111}
112
113/// Hierarchical expander decomposition
114///
115/// Maintains a multi-level decomposition where:
116/// - Each level contains φ-expander components
117/// - Lower levels have finer-grained components
118/// - Higher levels have coarser-grained components
119pub struct ExpanderDecomposition {
120    /// All components at each level
121    levels: Vec<Vec<ExpanderComponent>>,
122    /// Vertex to component mapping at each level
123    vertex_to_component: Vec<HashMap<VertexId, usize>>,
124    /// Target conductance threshold
125    phi: Conductance,
126    /// Graph reference
127    graph: Arc<DynamicGraph>,
128    /// Next component ID
129    next_component_id: usize,
130}
131
132impl ExpanderDecomposition {
133    /// Build expander decomposition with conductance threshold φ
134    ///
135    /// Constructs a hierarchical decomposition where each component
136    /// has conductance at least φ (or is maximal)
137    ///
138    /// # Arguments
139    ///
140    /// * `graph` - The graph to decompose
141    /// * `phi` - Conductance threshold (typically 1/polylog(n))
142    ///
143    /// # Returns
144    ///
145    /// A hierarchical expander decomposition
146    pub fn build(graph: Arc<DynamicGraph>, phi: Conductance) -> Result<Self> {
147        if phi <= 0.0 || phi >= 1.0 {
148            return Err(MinCutError::InvalidParameter(
149                format!("Conductance phi must be in (0, 1), got {}", phi)
150            ));
151        }
152
153        let mut decomp = Self {
154            levels: Vec::new(),
155            vertex_to_component: Vec::new(),
156            phi,
157            graph: graph.clone(),
158            next_component_id: 0,
159        };
160
161        // Build initial decomposition
162        decomp.build_hierarchy()?;
163
164        Ok(decomp)
165    }
166
167    /// Get component containing vertex at given level
168    pub fn component_at_level(&self, v: VertexId, level: usize) -> Option<&ExpanderComponent> {
169        if level >= self.levels.len() {
170            return None;
171        }
172
173        let component_id = self.vertex_to_component[level].get(&v)?;
174        self.levels[level].iter().find(|c| c.id == *component_id)
175    }
176
177    /// Update after edge insertion
178    ///
179    /// Identifies affected components and rebuilds them if necessary
180    pub fn insert_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
181        // Find affected components at each level
182        for level in 0..self.levels.len() {
183            if let (Some(u_comp_id), Some(v_comp_id)) = (
184                self.vertex_to_component[level].get(&u),
185                self.vertex_to_component[level].get(&v),
186            ) {
187                // If edge crosses components, update boundary edges
188                if u_comp_id != v_comp_id {
189                    // Find the edge ID
190                    if let Some(edge) = self.graph.get_edge(u, v) {
191                        // Add to boundary edges of both components
192                        for comp in &mut self.levels[level] {
193                            if comp.id == *u_comp_id || comp.id == *v_comp_id {
194                                comp.boundary_edges.push(edge.id);
195                            }
196                        }
197                    }
198                } else {
199                    // Edge within component - may affect conductance
200                    let comp_id = *u_comp_id;
201                    // Clone vertices to avoid borrow checker issues
202                    if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
203                        let vertices = comp.vertices.clone();
204                        let conductance = self.compute_conductance(&vertices);
205                        let volume = self.compute_volume(&vertices);
206
207                        // Update component
208                        if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id) {
209                            comp.conductance = conductance;
210                            comp.volume = volume;
211                        }
212                    }
213                }
214            }
215        }
216
217        Ok(())
218    }
219
220    /// Update after edge deletion
221    ///
222    /// Identifies affected components and rebuilds them if necessary
223    pub fn delete_edge(&mut self, u: VertexId, _v: VertexId) -> Result<()> {
224        // Find affected components at each level
225        for level in 0..self.levels.len() {
226            if let Some(u_comp_id) = self.vertex_to_component[level].get(&u) {
227                let comp_id = *u_comp_id;
228
229                // Edge deletion may disconnect component - check connectivity
230                // Clone vertices to avoid borrow checker issues
231                if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
232                    let vertices = comp.vertices.clone();
233                    let is_connected = self.is_connected(&vertices);
234
235                    if !is_connected {
236                        // Component disconnected - need to split it
237                        let _components = self.find_connected_components(&vertices);
238                        // This is a simplified approach - full implementation would
239                        // rebuild the entire level
240                    }
241
242                    // Recompute conductance and volume
243                    let conductance = self.compute_conductance(&vertices);
244                    let volume = self.compute_volume(&vertices);
245
246                    // Update component
247                    if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id) {
248                        comp.conductance = conductance;
249                        comp.volume = volume;
250                    }
251                }
252            }
253        }
254
255        Ok(())
256    }
257
258    /// Get number of levels in the decomposition
259    pub fn num_levels(&self) -> usize {
260        self.levels.len()
261    }
262
263    /// Build the hierarchical decomposition
264    fn build_hierarchy(&mut self) -> Result<()> {
265        let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
266
267        if all_vertices.is_empty() {
268            return Ok(());
269        }
270
271        // Build single level using deterministic decomposition
272        // Use deterministic decomposition to partition into expanders
273        let components = deterministic_decompose(&self.graph, &all_vertices, self.phi);
274
275        // Create expander components
276        let mut level_components = Vec::new();
277        let mut vertex_map = HashMap::new();
278
279        for vertices in components {
280            let comp_id = self.next_component_id;
281            self.next_component_id += 1;
282
283            let mut component = ExpanderComponent::new(comp_id, vertices.clone(), 0);
284            component.conductance = self.compute_conductance(&vertices);
285            component.volume = self.compute_volume(&vertices);
286
287            // Map vertices to this component
288            for &v in &vertices {
289                vertex_map.insert(v, comp_id);
290            }
291
292            level_components.push(component);
293        }
294
295        self.levels.push(level_components);
296        self.vertex_to_component.push(vertex_map);
297
298        Ok(())
299    }
300
301    /// Compute conductance of a vertex set
302    ///
303    /// φ(S) = cut(S, V\S) / min(vol(S), vol(V\S))
304    ///
305    /// where:
306    /// - cut(S, V\S) = sum of edge weights crossing between S and V\S
307    /// - vol(S) = sum of degrees of vertices in S
308    fn compute_conductance(&self, vertices: &HashSet<VertexId>) -> Conductance {
309        if vertices.is_empty() {
310            return 0.0;
311        }
312
313        let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
314        let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
315
316        if complement.is_empty() {
317            return 0.0;
318        }
319
320        // Compute cut value
321        let mut cut_value = 0.0;
322        for &u in vertices {
323            for (v, _) in self.graph.neighbors(u) {
324                if !vertices.contains(&v) {
325                    if let Some(weight) = self.graph.edge_weight(u, v) {
326                        cut_value += weight;
327                    }
328                }
329            }
330        }
331
332        // Compute volumes
333        let vol_s = self.compute_volume(vertices);
334        let vol_complement = self.compute_volume(&complement);
335
336        if vol_s == 0.0 || vol_complement == 0.0 {
337            return 0.0;
338        }
339
340        // φ(S) = cut / min(vol(S), vol(V\S))
341        let min_vol = vol_s.min(vol_complement);
342        cut_value / min_vol
343    }
344
345    /// Compute volume (sum of degrees) of a vertex set
346    fn compute_volume(&self, vertices: &HashSet<VertexId>) -> f64 {
347        vertices.iter()
348            .map(|&v| self.graph.degree(v) as f64)
349            .sum()
350    }
351
352    /// Expander pruning: find low-conductance cut
353    ///
354    /// Searches for a subset S with conductance < φ
355    /// Returns None if no such cut exists (component is a φ-expander)
356    fn prune(&self, component: &ExpanderComponent) -> Option<HashSet<VertexId>> {
357        // Try local cut search from each vertex
358        for &start in &component.vertices {
359            if let Some(cut) = self.local_cut_search(start, self.phi, &component.vertices) {
360                let conductance = self.compute_conductance(&cut);
361                if conductance < self.phi {
362                    return Some(cut);
363                }
364            }
365        }
366
367        None
368    }
369
370    /// Deterministic local search for low-conductance cut
371    ///
372    /// Performs BFS from start vertex, greedily adding vertices
373    /// to minimize conductance
374    fn local_cut_search(
375        &self,
376        start: VertexId,
377        target_conductance: Conductance,
378        vertices: &HashSet<VertexId>,
379    ) -> Option<HashSet<VertexId>> {
380        let mut current_set = HashSet::new();
381        current_set.insert(start);
382
383        let mut visited = HashSet::new();
384        visited.insert(start);
385
386        let mut queue = VecDeque::new();
387        queue.push_back(start);
388
389        let mut best_set = current_set.clone();
390        let mut best_conductance = self.compute_conductance(&current_set);
391
392        // BFS expansion
393        while let Some(u) = queue.pop_front() {
394            for (v, _) in self.graph.neighbors(u) {
395                // Only consider vertices in the component
396                if !vertices.contains(&v) || visited.contains(&v) {
397                    continue;
398                }
399
400                visited.insert(v);
401                current_set.insert(v);
402
403                let conductance = self.compute_conductance(&current_set);
404
405                // Keep track of best (lowest conductance) cut found
406                if conductance < best_conductance {
407                    best_conductance = conductance;
408                    best_set = current_set.clone();
409                }
410
411                // If we found a cut below threshold, return it
412                if conductance < target_conductance {
413                    return Some(current_set);
414                }
415
416                queue.push_back(v);
417
418                // Limit search size
419                if current_set.len() >= vertices.len() / 2 {
420                    break;
421                }
422            }
423        }
424
425        // Return best cut found if it's better than starting point
426        if best_conductance < self.phi {
427            Some(best_set)
428        } else {
429            None
430        }
431    }
432
433    /// Check if a set of vertices is connected
434    fn is_connected(&self, vertices: &HashSet<VertexId>) -> bool {
435        if vertices.is_empty() {
436            return true;
437        }
438
439        let start = *vertices.iter().next().unwrap();
440        let mut visited = HashSet::new();
441        let mut queue = VecDeque::new();
442
443        visited.insert(start);
444        queue.push_back(start);
445
446        while let Some(u) = queue.pop_front() {
447            for (v, _) in self.graph.neighbors(u) {
448                if vertices.contains(&v) && visited.insert(v) {
449                    queue.push_back(v);
450                }
451            }
452        }
453
454        visited.len() == vertices.len()
455    }
456
457    /// Find connected components within a vertex set
458    fn find_connected_components(&self, vertices: &HashSet<VertexId>) -> Vec<HashSet<VertexId>> {
459        let mut visited = HashSet::new();
460        let mut components = Vec::new();
461
462        for &start in vertices {
463            if visited.contains(&start) {
464                continue;
465            }
466
467            let mut component = HashSet::new();
468            let mut queue = VecDeque::new();
469
470            component.insert(start);
471            visited.insert(start);
472            queue.push_back(start);
473
474            while let Some(u) = queue.pop_front() {
475                for (v, _) in self.graph.neighbors(u) {
476                    if vertices.contains(&v) && visited.insert(v) {
477                        component.insert(v);
478                        queue.push_back(v);
479                    }
480                }
481            }
482
483            components.push(component);
484        }
485
486        components
487    }
488}
489
490/// Spectral gap computation for conductance estimation
491///
492/// Estimates the conductance of a component using random walk simulation
493/// This is a heuristic that approximates the true spectral conductance
494pub fn estimate_conductance(graph: &DynamicGraph, vertices: &HashSet<VertexId>) -> Conductance {
495    if vertices.is_empty() {
496        return 0.0;
497    }
498
499    let all_vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
500    let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
501
502    if complement.is_empty() {
503        return 0.0;
504    }
505
506    // Compute cut value
507    let mut cut_value = 0.0;
508    for &u in vertices {
509        for (v, _) in graph.neighbors(u) {
510            if !vertices.contains(&v) {
511                if let Some(weight) = graph.edge_weight(u, v) {
512                    cut_value += weight;
513                }
514            }
515        }
516    }
517
518    // Compute volumes
519    let vol_s: f64 = vertices.iter().map(|&v| graph.degree(v) as f64).sum();
520    let vol_complement: f64 = complement.iter().map(|&v| graph.degree(v) as f64).sum();
521
522    if vol_s == 0.0 || vol_complement == 0.0 {
523        return 0.0;
524    }
525
526    // φ(S) = cut / min(vol(S), vol(V\S))
527    let min_vol = vol_s.min(vol_complement);
528    cut_value / min_vol
529}
530
531/// Deterministic expander decomposition using trimming
532///
533/// Recursively partitions the graph into φ-expanders by:
534/// 1. Finding low-conductance cuts using local search
535/// 2. Removing them (trimming)
536/// 3. Recursing on remaining components
537pub fn deterministic_decompose(
538    graph: &DynamicGraph,
539    vertices: &HashSet<VertexId>,
540    phi: Conductance,
541) -> Vec<HashSet<VertexId>> {
542    // Base cases
543    if vertices.is_empty() {
544        return Vec::new();
545    }
546
547    if vertices.len() == 1 {
548        return vec![vertices.clone()];
549    }
550
551    // Try to find a low-conductance cut
552    if let Some(cut) = find_low_conductance_cut(graph, vertices, phi) {
553        // Ensure cut is not empty and not the whole set
554        if !cut.is_empty() && cut.len() < vertices.len() {
555            let complement: HashSet<VertexId> = vertices.difference(&cut).copied().collect();
556
557            // Recursively decompose both parts
558            let mut result = deterministic_decompose(graph, &cut, phi);
559            result.extend(deterministic_decompose(graph, &complement, phi));
560            return result;
561        }
562    }
563
564    // No valid low-conductance cut found - this is a φ-expander
565    vec![vertices.clone()]
566}
567
568/// Find a low-conductance cut using BFS-based local search
569fn find_low_conductance_cut(
570    graph: &DynamicGraph,
571    vertices: &HashSet<VertexId>,
572    phi: Conductance,
573) -> Option<HashSet<VertexId>> {
574    // Try starting from each vertex
575    for &start in vertices {
576        if let Some(cut) = bfs_local_search(graph, start, vertices, phi) {
577            let conductance = estimate_conductance(graph, &cut);
578            if conductance < phi {
579                return Some(cut);
580            }
581        }
582    }
583
584    None
585}
586
587/// BFS-based local search for low-conductance cuts
588fn bfs_local_search(
589    graph: &DynamicGraph,
590    start: VertexId,
591    vertices: &HashSet<VertexId>,
592    target_phi: Conductance,
593) -> Option<HashSet<VertexId>> {
594    let mut current_set = HashSet::new();
595    current_set.insert(start);
596
597    let mut visited = HashSet::new();
598    visited.insert(start);
599
600    let mut queue = VecDeque::new();
601    queue.push_back(start);
602
603    let mut best_set = current_set.clone();
604    let mut best_conductance = estimate_conductance(graph, &current_set);
605
606    while let Some(u) = queue.pop_front() {
607        for (v, _) in graph.neighbors(u) {
608            if !vertices.contains(&v) || visited.contains(&v) {
609                continue;
610            }
611
612            visited.insert(v);
613            current_set.insert(v);
614
615            let conductance = estimate_conductance(graph, &current_set);
616
617            if conductance < best_conductance {
618                best_conductance = conductance;
619                best_set = current_set.clone();
620            }
621
622            if conductance < target_phi {
623                return Some(current_set);
624            }
625
626            queue.push_back(v);
627
628            // Limit search size to half the component
629            if current_set.len() >= vertices.len() / 2 {
630                break;
631            }
632        }
633    }
634
635    if best_conductance < target_phi {
636        Some(best_set)
637    } else {
638        None
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    fn create_test_graph() -> Arc<DynamicGraph> {
647        let graph = Arc::new(DynamicGraph::new());
648        // Create a triangle
649        graph.insert_edge(1, 2, 1.0).unwrap();
650        graph.insert_edge(2, 3, 1.0).unwrap();
651        graph.insert_edge(3, 1, 1.0).unwrap();
652        graph
653    }
654
655    fn create_expander_graph() -> Arc<DynamicGraph> {
656        let graph = Arc::new(DynamicGraph::new());
657        // Create a well-connected graph (complete graph on 5 vertices)
658        for i in 1..=5 {
659            for j in (i+1)..=5 {
660                graph.insert_edge(i, j, 1.0).unwrap();
661            }
662        }
663        graph
664    }
665
666    fn create_separable_graph() -> Arc<DynamicGraph> {
667        let graph = Arc::new(DynamicGraph::new());
668        // Two triangles connected by single edge
669        // Triangle 1: 1-2-3
670        graph.insert_edge(1, 2, 1.0).unwrap();
671        graph.insert_edge(2, 3, 1.0).unwrap();
672        graph.insert_edge(3, 1, 1.0).unwrap();
673        // Bridge
674        graph.insert_edge(3, 4, 1.0).unwrap();
675        // Triangle 2: 4-5-6
676        graph.insert_edge(4, 5, 1.0).unwrap();
677        graph.insert_edge(5, 6, 1.0).unwrap();
678        graph.insert_edge(6, 4, 1.0).unwrap();
679        graph
680    }
681
682    #[test]
683    fn test_build_simple() {
684        let graph = create_test_graph();
685        let phi = 0.1;
686        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
687
688        assert!(decomp.num_levels() > 0);
689    }
690
691    #[test]
692    fn test_build_invalid_phi() {
693        let graph = create_test_graph();
694
695        // Test phi = 0
696        assert!(ExpanderDecomposition::build(graph.clone(), 0.0).is_err());
697
698        // Test phi = 1
699        assert!(ExpanderDecomposition::build(graph.clone(), 1.0).is_err());
700
701        // Test phi > 1
702        assert!(ExpanderDecomposition::build(graph.clone(), 1.5).is_err());
703    }
704
705    #[test]
706    fn test_compute_conductance_triangle() {
707        let graph = create_test_graph();
708        let phi = 0.1;
709        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
710
711        // Single vertex has conductance based on its cut
712        let mut single_vertex = HashSet::new();
713        single_vertex.insert(1);
714        let conductance = decomp.compute_conductance(&single_vertex);
715
716        // Vertex 1 has degree 2, neighbors are 2 and 3
717        // cut({1}, {2,3}) = 2 (both edges from 1)
718        // vol({1}) = 2, vol({2,3}) = 4
719        // φ = 2 / min(2, 4) = 2 / 2 = 1.0
720        assert_eq!(conductance, 1.0);
721    }
722
723    #[test]
724    fn test_compute_conductance_complete() {
725        let graph = create_expander_graph();
726        let phi = 0.1;
727        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
728
729        // Single vertex in complete graph K5
730        let mut single_vertex = HashSet::new();
731        single_vertex.insert(1);
732        let conductance = decomp.compute_conductance(&single_vertex);
733
734        // In K5, each vertex has degree 4
735        // cut({1}, {2,3,4,5}) = 4
736        // vol({1}) = 4, vol({2,3,4,5}) = 16
737        // φ = 4 / min(4, 16) = 4 / 4 = 1.0
738        assert_eq!(conductance, 1.0);
739    }
740
741    #[test]
742    fn test_compute_volume() {
743        let graph = create_test_graph();
744        let phi = 0.1;
745        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
746
747        let mut vertices = HashSet::new();
748        vertices.insert(1);
749        vertices.insert(2);
750
751        // Each vertex in triangle has degree 2
752        // Total volume = 2 + 2 = 4
753        let volume = decomp.compute_volume(&vertices);
754        assert_eq!(volume, 4.0);
755    }
756
757    #[test]
758    fn test_estimate_conductance() {
759        let graph = create_test_graph();
760
761        let mut vertices = HashSet::new();
762        vertices.insert(1);
763
764        let conductance = estimate_conductance(&graph, &vertices);
765        assert_eq!(conductance, 1.0);
766    }
767
768    #[test]
769    fn test_deterministic_decompose_triangle() {
770        let graph = create_test_graph();
771        let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
772
773        // Low phi will find cuts and decompose
774        // In a triangle, any single vertex has conductance 1.0
775        // (cut of 2 edges, volume of 2: φ = 2/2 = 1.0)
776        let phi = 0.5;
777        let components = deterministic_decompose(&graph, &vertices, phi);
778
779        // With phi=0.5, should split into smaller components
780        // since conductance of single vertices (1.0) > phi (0.5)
781        assert!(components.len() >= 1);
782        assert_eq!(components.iter().map(|c| c.len()).sum::<usize>(), 3);
783    }
784
785    #[test]
786    fn test_deterministic_decompose_separable() {
787        let graph = create_separable_graph();
788        let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
789
790        // Low phi should find the bridge cut
791        let phi = 0.3;
792        let components = deterministic_decompose(&graph, &vertices, phi);
793
794        // Should separate into at least 2 components
795        assert!(components.len() >= 1);
796    }
797
798    #[test]
799    fn test_component_at_level() {
800        let graph = create_test_graph();
801        let phi = 0.1;
802        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
803
804        // Query component at level 0
805        if decomp.num_levels() > 0 {
806            let comp = decomp.component_at_level(1, 0);
807            assert!(comp.is_some());
808
809            if let Some(c) = comp {
810                assert!(c.contains(1));
811            }
812        }
813    }
814
815    #[test]
816    fn test_insert_edge() {
817        let graph = create_test_graph();
818        let phi = 0.1;
819        let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
820
821        // Add a new edge
822        graph.insert_edge(1, 4, 1.0).unwrap();
823        let result = decomp.insert_edge(1, 4);
824        assert!(result.is_ok());
825    }
826
827    #[test]
828    fn test_delete_edge() {
829        let graph = create_test_graph();
830        let phi = 0.1;
831        let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
832
833        // Delete an edge
834        graph.delete_edge(1, 2).unwrap();
835        let result = decomp.delete_edge(1, 2);
836        assert!(result.is_ok());
837    }
838
839    #[test]
840    fn test_is_connected() {
841        let graph = create_test_graph();
842        let phi = 0.1;
843        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
844
845        let vertices: HashSet<VertexId> = vec![1, 2, 3].into_iter().collect();
846        assert!(decomp.is_connected(&vertices));
847
848        // Disconnected set
849        let disconnected: HashSet<VertexId> = vec![1, 2].into_iter().collect();
850        // After removing edge 3, vertices 1-2 are still connected
851        assert!(decomp.is_connected(&disconnected));
852    }
853
854    #[test]
855    fn test_find_connected_components() {
856        let graph = Arc::new(DynamicGraph::new());
857        // Two separate edges
858        graph.insert_edge(1, 2, 1.0).unwrap();
859        graph.insert_edge(3, 4, 1.0).unwrap();
860
861        let phi = 0.1;
862        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
863
864        let vertices: HashSet<VertexId> = vec![1, 2, 3, 4].into_iter().collect();
865        let components = decomp.find_connected_components(&vertices);
866
867        assert_eq!(components.len(), 2);
868    }
869
870    #[test]
871    fn test_local_cut_search() {
872        let graph = create_separable_graph();
873        let phi = 0.3;
874        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
875
876        let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
877
878        // Search from vertex 1
879        if let Some(cut) = decomp.local_cut_search(1, phi, &vertices) {
880            assert!(!cut.is_empty());
881            assert!(cut.len() < vertices.len());
882        }
883    }
884
885    #[test]
886    fn test_prune() {
887        let graph = create_separable_graph();
888        let phi = 0.3;
889        let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
890
891        if decomp.num_levels() > 0 && !decomp.levels[0].is_empty() {
892            let component = &decomp.levels[0][0];
893
894            // Try to prune - may or may not find a cut depending on structure
895            let result = decomp.prune(component);
896            // Just verify it doesn't crash
897            assert!(result.is_some() || result.is_none());
898        }
899    }
900
901    #[test]
902    fn test_expander_component_methods() {
903        let mut vertices = HashSet::new();
904        vertices.insert(1);
905        vertices.insert(2);
906        vertices.insert(3);
907
908        let component = ExpanderComponent::new(0, vertices, 0);
909
910        assert_eq!(component.id, 0);
911        assert_eq!(component.level, 0);
912        assert_eq!(component.size(), 3);
913        assert!(component.contains(1));
914        assert!(!component.contains(4));
915    }
916
917    #[test]
918    fn test_empty_graph() {
919        let graph = Arc::new(DynamicGraph::new());
920        let phi = 0.1;
921        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
922
923        assert_eq!(decomp.num_levels(), 0);
924    }
925
926    #[test]
927    fn test_single_vertex() {
928        let graph = Arc::new(DynamicGraph::new());
929        graph.add_vertex(1);
930
931        let phi = 0.1;
932        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
933
934        assert!(decomp.num_levels() > 0);
935    }
936
937    #[test]
938    fn test_large_expander() {
939        let graph = Arc::new(DynamicGraph::new());
940
941        // Create a larger complete graph
942        for i in 1..=10 {
943            for j in (i+1)..=10 {
944                graph.insert_edge(i, j, 1.0).unwrap();
945            }
946        }
947
948        let phi = 0.1;
949        let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
950
951        assert!(decomp.num_levels() > 0);
952    }
953}