ruvector_mincut/optimization/
dspar.rs1use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14
15#[derive(Debug, Clone)]
17pub struct PresparseConfig {
18 pub target_sparsity: f64,
20 pub resistance_threshold: f64,
22 pub adaptive_threshold: bool,
24 pub max_edges: Option<usize>,
26 pub seed: Option<u64>,
28}
29
30impl Default for PresparseConfig {
31 fn default() -> Self {
32 Self {
33 target_sparsity: 0.1, resistance_threshold: 0.0,
35 adaptive_threshold: true,
36 max_edges: None,
37 seed: Some(42),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct PresparseStats {
45 pub original_edges: usize,
47 pub sparse_edges: usize,
49 pub sparsity_ratio: f64,
51 pub time_us: u64,
53 pub speedup_factor: f64,
55 pub vertices_processed: usize,
57}
58
59#[derive(Debug)]
61pub struct PresparseResult {
62 pub edges: Vec<(VertexId, VertexId, Weight)>,
64 pub edge_mapping: HashMap<usize, EdgeId>,
66 pub stats: PresparseStats,
68}
69
70pub struct DegreePresparse {
75 config: PresparseConfig,
76 degree_cache: HashMap<VertexId, usize>,
78}
79
80impl DegreePresparse {
81 pub fn new() -> Self {
83 Self::with_config(PresparseConfig::default())
84 }
85
86 pub fn with_config(config: PresparseConfig) -> Self {
88 Self {
89 config,
90 degree_cache: HashMap::new(),
91 }
92 }
93
94 #[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; }
105 1.0 / (deg_u as f64 * deg_v as f64)
106 }
107
108 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 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 let avg_degree = (2 * m) as f64 / n as f64;
127
128 let target_edges = (n as f64 * (n as f64).ln()).min(m as f64);
130
131 let sparsity = target_edges / m as f64;
134
135 1.0 / (avg_degree * avg_degree * sparsity.max(0.01))
137 }
138
139 pub fn presparse(&mut self, graph: &DynamicGraph) -> PresparseResult {
144 let start = std::time::Instant::now();
145
146 self.precompute_degrees(graph);
148
149 let original_edges = graph.num_edges();
150
151 let threshold = if self.config.adaptive_threshold {
153 self.compute_adaptive_threshold(graph)
154 } else {
155 self.config.resistance_threshold
156 };
157
158 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 scored_edges.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal));
172
173 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 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 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 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); 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 #[inline]
236 fn sampling_probability(&self, resistance: f64, threshold: f64) -> f64 {
237 if resistance >= threshold {
238 1.0 } else {
240 (resistance / threshold).max(0.01)
242 }
243 }
244
245 pub fn should_include_edge(&mut self, graph: &DynamicGraph, u: VertexId, v: VertexId) -> bool {
249 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 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
278pub struct SpectralConcordance {
282 pub lambda_laplacian: f64,
284 pub lambda_feature: f64,
286 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 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 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 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 fn feature_preservation_loss(&self, original: &DynamicGraph, sparse: &DynamicGraph) -> f64 {
342 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 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 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 assert!(dspar.effective_resistance(10, 10) < dspar.effective_resistance(2, 2));
393
394 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 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 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 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 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 for (idx, _) in result.edges.iter().enumerate() {
496 assert!(result.edge_mapping.contains_key(&idx));
497 }
498 }
499}