Skip to main content

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::error::{MinCutError, Result};
60use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
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(format!(
149                "Conductance phi must be in (0, 1), got {}",
150                phi
151            )));
152        }
153
154        let mut decomp = Self {
155            levels: Vec::new(),
156            vertex_to_component: Vec::new(),
157            phi,
158            graph: graph.clone(),
159            next_component_id: 0,
160        };
161
162        // Build initial decomposition
163        decomp.build_hierarchy()?;
164
165        Ok(decomp)
166    }
167
168    /// Get component containing vertex at given level
169    pub fn component_at_level(&self, v: VertexId, level: usize) -> Option<&ExpanderComponent> {
170        if level >= self.levels.len() {
171            return None;
172        }
173
174        let component_id = self.vertex_to_component[level].get(&v)?;
175        self.levels[level].iter().find(|c| c.id == *component_id)
176    }
177
178    /// Update after edge insertion
179    ///
180    /// Identifies affected components and rebuilds them if necessary
181    pub fn insert_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
182        // Find affected components at each level
183        for level in 0..self.levels.len() {
184            if let (Some(u_comp_id), Some(v_comp_id)) = (
185                self.vertex_to_component[level].get(&u),
186                self.vertex_to_component[level].get(&v),
187            ) {
188                // If edge crosses components, update boundary edges
189                if u_comp_id != v_comp_id {
190                    // Find the edge ID
191                    if let Some(edge) = self.graph.get_edge(u, v) {
192                        // Add to boundary edges of both components
193                        for comp in &mut self.levels[level] {
194                            if comp.id == *u_comp_id || comp.id == *v_comp_id {
195                                comp.boundary_edges.push(edge.id);
196                            }
197                        }
198                    }
199                } else {
200                    // Edge within component - may affect conductance
201                    let comp_id = *u_comp_id;
202                    // Clone vertices to avoid borrow checker issues
203                    if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
204                        let vertices = comp.vertices.clone();
205                        let conductance = self.compute_conductance(&vertices);
206                        let volume = self.compute_volume(&vertices);
207
208                        // Update component
209                        if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id)
210                        {
211                            comp.conductance = conductance;
212                            comp.volume = volume;
213                        }
214                    }
215                }
216            }
217        }
218
219        Ok(())
220    }
221
222    /// Update after edge deletion
223    ///
224    /// Identifies affected components and rebuilds them if necessary
225    pub fn delete_edge(&mut self, u: VertexId, _v: VertexId) -> Result<()> {
226        // Find affected components at each level
227        for level in 0..self.levels.len() {
228            if let Some(u_comp_id) = self.vertex_to_component[level].get(&u) {
229                let comp_id = *u_comp_id;
230
231                // Edge deletion may disconnect component - check connectivity
232                // Clone vertices to avoid borrow checker issues
233                if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
234                    let vertices = comp.vertices.clone();
235                    let is_connected = self.is_connected(&vertices);
236
237                    if !is_connected {
238                        // Component disconnected - need to split it
239                        let _components = self.find_connected_components(&vertices);
240                        // This is a simplified approach - full implementation would
241                        // rebuild the entire level
242                    }
243
244                    // Recompute conductance and volume
245                    let conductance = self.compute_conductance(&vertices);
246                    let volume = self.compute_volume(&vertices);
247
248                    // Update component
249                    if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id) {
250                        comp.conductance = conductance;
251                        comp.volume = volume;
252                    }
253                }
254            }
255        }
256
257        Ok(())
258    }
259
260    /// Get number of levels in the decomposition
261    pub fn num_levels(&self) -> usize {
262        self.levels.len()
263    }
264
265    /// Build the hierarchical decomposition
266    fn build_hierarchy(&mut self) -> Result<()> {
267        let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
268
269        if all_vertices.is_empty() {
270            return Ok(());
271        }
272
273        // Build single level using deterministic decomposition
274        // Use deterministic decomposition to partition into expanders
275        let components = deterministic_decompose(&self.graph, &all_vertices, self.phi);
276
277        // Create expander components
278        let mut level_components = Vec::new();
279        let mut vertex_map = HashMap::new();
280
281        for vertices in components {
282            let comp_id = self.next_component_id;
283            self.next_component_id += 1;
284
285            let mut component = ExpanderComponent::new(comp_id, vertices.clone(), 0);
286            component.conductance = self.compute_conductance(&vertices);
287            component.volume = self.compute_volume(&vertices);
288
289            // Map vertices to this component
290            for &v in &vertices {
291                vertex_map.insert(v, comp_id);
292            }
293
294            level_components.push(component);
295        }
296
297        self.levels.push(level_components);
298        self.vertex_to_component.push(vertex_map);
299
300        Ok(())
301    }
302
303    /// Compute conductance of a vertex set
304    ///
305    /// φ(S) = cut(S, V\S) / min(vol(S), vol(V\S))
306    ///
307    /// where:
308    /// - cut(S, V\S) = sum of edge weights crossing between S and V\S
309    /// - vol(S) = sum of degrees of vertices in S
310    fn compute_conductance(&self, vertices: &HashSet<VertexId>) -> Conductance {
311        if vertices.is_empty() {
312            return 0.0;
313        }
314
315        let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
316        let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
317
318        if complement.is_empty() {
319            return 0.0;
320        }
321
322        // Compute cut value
323        let mut cut_value = 0.0;
324        for &u in vertices {
325            for (v, _) in self.graph.neighbors(u) {
326                if !vertices.contains(&v) {
327                    if let Some(weight) = self.graph.edge_weight(u, v) {
328                        cut_value += weight;
329                    }
330                }
331            }
332        }
333
334        // Compute volumes
335        let vol_s = self.compute_volume(vertices);
336        let vol_complement = self.compute_volume(&complement);
337
338        if vol_s == 0.0 || vol_complement == 0.0 {
339            return 0.0;
340        }
341
342        // φ(S) = cut / min(vol(S), vol(V\S))
343        let min_vol = vol_s.min(vol_complement);
344        cut_value / min_vol
345    }
346
347    /// Compute volume (sum of degrees) of a vertex set
348    fn compute_volume(&self, vertices: &HashSet<VertexId>) -> f64 {
349        vertices.iter().map(|&v| self.graph.degree(v) as f64).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}