Skip to main content

ruvector_mincut/optimization/
dspar.rs

1//! Degree-based Presparse (DSpar) Implementation
2//!
3//! Fast approximation for sparsification using effective resistance:
4//!     R_eff(u,v) ≈ 1 / (deg(u) × deg(v))
5//!
6//! This provides a 5.9x speedup over exact effective resistance computation
7//! while maintaining spectral properties for minimum cut preservation.
8//!
9//! Reference: "Degree-based Sparsification" (OpenReview)
10
11use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14
15/// Configuration for degree-based presparse
16#[derive(Debug, Clone)]
17pub struct PresparseConfig {
18    /// Target sparsity ratio (0.0-1.0, lower = more sparse)
19    pub target_sparsity: f64,
20    /// Minimum effective resistance threshold for keeping edges
21    pub resistance_threshold: f64,
22    /// Whether to use adaptive threshold based on graph density
23    pub adaptive_threshold: bool,
24    /// Maximum edges to keep (optional hard limit)
25    pub max_edges: Option<usize>,
26    /// Random seed for probabilistic sampling
27    pub seed: Option<u64>,
28}
29
30impl Default for PresparseConfig {
31    fn default() -> Self {
32        Self {
33            target_sparsity: 0.1, // Keep ~10% of edges
34            resistance_threshold: 0.0,
35            adaptive_threshold: true,
36            max_edges: None,
37            seed: Some(42),
38        }
39    }
40}
41
42/// Statistics from presparse operation
43#[derive(Debug, Clone, Default)]
44pub struct PresparseStats {
45    /// Original number of edges
46    pub original_edges: usize,
47    /// Number of edges after presparse
48    pub sparse_edges: usize,
49    /// Sparsity ratio achieved
50    pub sparsity_ratio: f64,
51    /// Time taken in microseconds
52    pub time_us: u64,
53    /// Estimated speedup factor
54    pub speedup_factor: f64,
55    /// Number of vertices affected
56    pub vertices_processed: usize,
57}
58
59/// Result of presparse operation
60#[derive(Debug)]
61pub struct PresparseResult {
62    /// Sparsified edges with scaled weights
63    pub edges: Vec<(VertexId, VertexId, Weight)>,
64    /// Mapping from new edge index to original edge ID
65    pub edge_mapping: HashMap<usize, EdgeId>,
66    /// Statistics
67    pub stats: PresparseStats,
68}
69
70/// Degree-based presparse implementation
71///
72/// Uses effective resistance approximation R_eff(u,v) ≈ 1/(deg_u × deg_v)
73/// to pre-filter edges before exact sparsification, achieving 5.9x speedup.
74pub struct DegreePresparse {
75    config: PresparseConfig,
76    /// Cached degree information
77    degree_cache: HashMap<VertexId, usize>,
78}
79
80impl DegreePresparse {
81    /// Create new degree presparse with default config
82    pub fn new() -> Self {
83        Self::with_config(PresparseConfig::default())
84    }
85
86    /// Create with custom config
87    pub fn with_config(config: PresparseConfig) -> Self {
88        Self {
89            config,
90            degree_cache: HashMap::new(),
91        }
92    }
93
94    /// Compute effective resistance approximation for an edge
95    ///
96    /// R_eff(u,v) ≈ 1 / (deg(u) × deg(v))
97    ///
98    /// High resistance = edge is important for connectivity
99    /// Low resistance = edge can likely be removed
100    #[inline]
101    pub fn effective_resistance(&self, deg_u: usize, deg_v: usize) -> f64 {
102        if deg_u == 0 || deg_v == 0 {
103            return f64::INFINITY; // Always keep edges to isolated vertices
104        }
105        1.0 / (deg_u as f64 * deg_v as f64)
106    }
107
108    /// Pre-compute degrees for all vertices
109    fn precompute_degrees(&mut self, graph: &DynamicGraph) {
110        self.degree_cache.clear();
111        for v in graph.vertices() {
112            self.degree_cache.insert(v, graph.degree(v));
113        }
114    }
115
116    /// Compute adaptive threshold based on graph properties
117    fn compute_adaptive_threshold(&self, graph: &DynamicGraph) -> f64 {
118        let n = graph.num_vertices();
119        let m = graph.num_edges();
120
121        if n == 0 || m == 0 {
122            return 0.0;
123        }
124
125        // Average degree
126        let avg_degree = (2 * m) as f64 / n as f64;
127
128        // Target: keep O(n log n) edges
129        let target_edges = (n as f64 * (n as f64).ln()).min(m as f64);
130
131        // Compute threshold that keeps approximately target_edges
132        // Higher threshold = fewer edges kept
133        let sparsity = target_edges / m as f64;
134
135        // Threshold based on average effective resistance
136        1.0 / (avg_degree * avg_degree * sparsity.max(0.01))
137    }
138
139    /// Perform degree-based presparse on a graph
140    ///
141    /// Returns a sparsified edge set that preserves spectral properties
142    /// for minimum cut computation.
143    pub fn presparse(&mut self, graph: &DynamicGraph) -> PresparseResult {
144        let start = std::time::Instant::now();
145
146        // Pre-compute degrees
147        self.precompute_degrees(graph);
148
149        let original_edges = graph.num_edges();
150
151        // Compute threshold
152        let threshold = if self.config.adaptive_threshold {
153            self.compute_adaptive_threshold(graph)
154        } else {
155            self.config.resistance_threshold
156        };
157
158        // Score all edges by effective resistance
159        let mut scored_edges: Vec<(EdgeId, VertexId, VertexId, Weight, f64)> =
160            Vec::with_capacity(original_edges);
161
162        for edge in graph.edges() {
163            let deg_u = *self.degree_cache.get(&edge.source).unwrap_or(&1);
164            let deg_v = *self.degree_cache.get(&edge.target).unwrap_or(&1);
165            let resistance = self.effective_resistance(deg_u, deg_v);
166
167            scored_edges.push((edge.id, edge.source, edge.target, edge.weight, resistance));
168        }
169
170        // Sort by resistance (descending - high resistance = important)
171        scored_edges.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal));
172
173        // Determine how many edges to keep
174        let target_count = if let Some(max) = self.config.max_edges {
175            max.min(original_edges)
176        } else {
177            ((original_edges as f64 * self.config.target_sparsity).ceil() as usize).max(1)
178        };
179
180        // Keep edges with highest effective resistance
181        let mut result_edges = Vec::with_capacity(target_count);
182        let mut edge_mapping = HashMap::with_capacity(target_count);
183        let mut kept_vertices = HashSet::new();
184
185        for (idx, (edge_id, u, v, weight, resistance)) in scored_edges.into_iter().enumerate() {
186            if result_edges.len() >= target_count && resistance < threshold {
187                break;
188            }
189
190            // Scale weight by inverse sampling probability
191            let sampling_prob = self.sampling_probability(resistance, threshold);
192            let scaled_weight = if sampling_prob > 0.0 {
193                weight / sampling_prob
194            } else {
195                weight
196            };
197
198            result_edges.push((u, v, scaled_weight));
199            edge_mapping.insert(result_edges.len() - 1, edge_id);
200            kept_vertices.insert(u);
201            kept_vertices.insert(v);
202
203            if result_edges.len() >= target_count {
204                break;
205            }
206        }
207
208        let elapsed_us = start.elapsed().as_micros() as u64;
209        let sparse_edges = result_edges.len();
210
211        // Estimate speedup: O(m) -> O(m') where m' << m
212        // Plus the 5.9x from avoiding exact resistance computation
213        let sparsity_speedup = if sparse_edges > 0 {
214            original_edges as f64 / sparse_edges as f64
215        } else {
216            1.0
217        };
218        let speedup_factor = sparsity_speedup.min(5.9); // Cap at theoretical DSpar speedup
219
220        PresparseResult {
221            edges: result_edges,
222            edge_mapping,
223            stats: PresparseStats {
224                original_edges,
225                sparse_edges,
226                sparsity_ratio: sparse_edges as f64 / original_edges.max(1) as f64,
227                time_us: elapsed_us,
228                speedup_factor,
229                vertices_processed: kept_vertices.len(),
230            },
231        }
232    }
233
234    /// Compute sampling probability for an edge
235    #[inline]
236    fn sampling_probability(&self, resistance: f64, threshold: f64) -> f64 {
237        if resistance >= threshold {
238            1.0 // Always keep high-resistance edges
239        } else {
240            // Probability proportional to resistance
241            (resistance / threshold).max(0.01)
242        }
243    }
244
245    /// Incremental update: handle edge insertion
246    ///
247    /// Returns whether the edge should be included in the sparse graph
248    pub fn should_include_edge(&mut self, graph: &DynamicGraph, u: VertexId, v: VertexId) -> bool {
249        // Update degree cache
250        self.degree_cache.insert(u, graph.degree(u));
251        self.degree_cache.insert(v, graph.degree(v));
252
253        let deg_u = *self.degree_cache.get(&u).unwrap_or(&1);
254        let deg_v = *self.degree_cache.get(&v).unwrap_or(&1);
255        let resistance = self.effective_resistance(deg_u, deg_v);
256
257        let threshold = if self.config.adaptive_threshold {
258            self.compute_adaptive_threshold(graph)
259        } else {
260            self.config.resistance_threshold
261        };
262
263        resistance >= threshold
264    }
265
266    /// Get statistics for the presparse
267    pub fn config(&self) -> &PresparseConfig {
268        &self.config
269    }
270}
271
272impl Default for DegreePresparse {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278/// Spectral concordance loss for validating sparsification quality
279///
280/// L = λ₁·Laplacian_Alignment + λ₂·Feature_Preserve + λ₃·Sparsity
281pub struct SpectralConcordance {
282    /// Weight for Laplacian alignment term
283    pub lambda_laplacian: f64,
284    /// Weight for feature preservation term
285    pub lambda_feature: f64,
286    /// Weight for sparsity inducing term
287    pub lambda_sparsity: f64,
288}
289
290impl Default for SpectralConcordance {
291    fn default() -> Self {
292        Self {
293            lambda_laplacian: 1.0,
294            lambda_feature: 0.5,
295            lambda_sparsity: 0.1,
296        }
297    }
298}
299
300impl SpectralConcordance {
301    /// Compute the spectral concordance loss between original and sparse graphs
302    pub fn compute_loss(&self, original: &DynamicGraph, sparse: &DynamicGraph) -> f64 {
303        let laplacian_loss = self.laplacian_alignment_loss(original, sparse);
304        let feature_loss = self.feature_preservation_loss(original, sparse);
305        let sparsity_loss = self.sparsity_loss(original, sparse);
306
307        self.lambda_laplacian * laplacian_loss
308            + self.lambda_feature * feature_loss
309            + self.lambda_sparsity * sparsity_loss
310    }
311
312    /// Approximate Laplacian alignment loss using degree distribution
313    fn laplacian_alignment_loss(&self, original: &DynamicGraph, sparse: &DynamicGraph) -> f64 {
314        let orig_vertices = original.vertices();
315        if orig_vertices.is_empty() {
316            return 0.0;
317        }
318
319        let mut total_diff = 0.0;
320        let mut count = 0;
321
322        for v in orig_vertices {
323            let orig_deg = original.degree(v) as f64;
324            let sparse_deg = sparse.degree(v) as f64;
325
326            if orig_deg > 0.0 {
327                // Relative degree difference
328                total_diff += ((orig_deg - sparse_deg) / orig_deg).abs();
329                count += 1;
330            }
331        }
332
333        if count > 0 {
334            total_diff / count as f64
335        } else {
336            0.0
337        }
338    }
339
340    /// Feature preservation loss (cut value approximation)
341    fn feature_preservation_loss(&self, original: &DynamicGraph, sparse: &DynamicGraph) -> f64 {
342        // Compare minimum degree (crude cut approximation)
343        let orig_min_deg = original
344            .vertices()
345            .iter()
346            .map(|&v| original.degree(v))
347            .min()
348            .unwrap_or(0) as f64;
349
350        let sparse_min_deg = sparse
351            .vertices()
352            .iter()
353            .map(|&v| sparse.degree(v))
354            .min()
355            .unwrap_or(0) as f64;
356
357        if orig_min_deg > 0.0 {
358            ((orig_min_deg - sparse_min_deg) / orig_min_deg).abs()
359        } else {
360            0.0
361        }
362    }
363
364    /// Sparsity inducing loss
365    fn sparsity_loss(&self, original: &DynamicGraph, sparse: &DynamicGraph) -> f64 {
366        let orig_edges = original.num_edges().max(1) as f64;
367        let sparse_edges = sparse.num_edges() as f64;
368        sparse_edges / orig_edges
369    }
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    fn create_test_graph() -> DynamicGraph {
377        let g = DynamicGraph::new();
378        // Create a dense graph
379        for i in 1..=10 {
380            for j in (i + 1)..=10 {
381                let _ = g.insert_edge(i, j, 1.0);
382            }
383        }
384        g
385    }
386
387    #[test]
388    fn test_effective_resistance() {
389        let dspar = DegreePresparse::new();
390
391        // High degree vertices -> low resistance
392        assert!(dspar.effective_resistance(10, 10) < dspar.effective_resistance(2, 2));
393
394        // Zero degree -> infinity
395        assert!(dspar.effective_resistance(0, 5).is_infinite());
396    }
397
398    #[test]
399    fn test_presparse_reduces_edges() {
400        let graph = create_test_graph();
401        let original_edges = graph.num_edges();
402
403        let mut dspar = DegreePresparse::with_config(PresparseConfig {
404            target_sparsity: 0.3,
405            ..Default::default()
406        });
407
408        let result = dspar.presparse(&graph);
409
410        assert!(result.stats.sparse_edges < original_edges);
411        assert!(result.stats.sparsity_ratio <= 0.5);
412        assert!(result.stats.speedup_factor > 1.0);
413    }
414
415    #[test]
416    fn test_presparse_preserves_connectivity() {
417        let graph = create_test_graph();
418
419        let mut dspar = DegreePresparse::with_config(PresparseConfig {
420            target_sparsity: 0.2,
421            ..Default::default()
422        });
423
424        let result = dspar.presparse(&graph);
425
426        // Should keep at least n-1 edges to maintain connectivity
427        assert!(result.stats.sparse_edges >= graph.num_vertices() - 1);
428    }
429
430    #[test]
431    fn test_adaptive_threshold() {
432        let graph = create_test_graph();
433
434        let mut dspar = DegreePresparse::with_config(PresparseConfig {
435            adaptive_threshold: true,
436            ..Default::default()
437        });
438
439        dspar.precompute_degrees(&graph);
440        let threshold = dspar.compute_adaptive_threshold(&graph);
441
442        assert!(threshold > 0.0);
443    }
444
445    #[test]
446    fn test_spectral_concordance() {
447        let original = create_test_graph();
448
449        let mut dspar = DegreePresparse::with_config(PresparseConfig {
450            target_sparsity: 0.5,
451            ..Default::default()
452        });
453
454        let result = dspar.presparse(&original);
455
456        // Create sparse graph
457        let sparse = DynamicGraph::new();
458        for (u, v, w) in &result.edges {
459            let _ = sparse.insert_edge(*u, *v, *w);
460        }
461
462        let concordance = SpectralConcordance::default();
463        let loss = concordance.compute_loss(&original, &sparse);
464
465        // Loss should be bounded
466        assert!(loss >= 0.0);
467        assert!(loss < 10.0);
468    }
469
470    #[test]
471    fn test_should_include_edge() {
472        let graph = DynamicGraph::new();
473        graph.insert_edge(1, 2, 1.0).unwrap();
474        graph.insert_edge(2, 3, 1.0).unwrap();
475
476        let mut dspar = DegreePresparse::with_config(PresparseConfig {
477            resistance_threshold: 0.0,
478            adaptive_threshold: false,
479            ..Default::default()
480        });
481
482        // New edge to low-degree vertices should be included
483        let should_include = dspar.should_include_edge(&graph, 1, 3);
484        assert!(should_include);
485    }
486
487    #[test]
488    fn test_edge_mapping() {
489        let graph = create_test_graph();
490
491        let mut dspar = DegreePresparse::new();
492        let result = dspar.presparse(&graph);
493
494        // Each sparse edge should map to an original edge
495        for (idx, _) in result.edges.iter().enumerate() {
496            assert!(result.edge_mapping.contains_key(&idx));
497        }
498    }
499}