Skip to main content

ruvector_sparsifier/
sparsifier.rs

1//! Main adaptive spectral sparsifier (AdaptiveGeoSpar).
2//!
3//! Maintains a compressed shadow graph `g_spec` that preserves the Laplacian
4//! energy of the full graph `g_full` within `(1 +/- epsilon)`. Supports
5//! dynamic edge insertions, deletions, and periodic spectral audits.
6
7use std::collections::HashSet;
8
9use parking_lot::RwLock;
10use tracing::{debug, info, warn};
11
12use crate::audit::SpectralAuditor;
13use crate::backbone::Backbone;
14use crate::error::{Result, SparsifierError};
15use crate::graph::SparseGraph;
16use crate::importance::LocalImportanceScorer;
17use crate::sampler::SpectralSampler;
18use crate::traits::{BackboneStrategy, ImportanceScorer, Sparsifier};
19use crate::types::{AuditResult, SparsifierConfig, SparsifierStats};
20
21// ---------------------------------------------------------------------------
22// AdaptiveGeoSpar
23// ---------------------------------------------------------------------------
24
25/// Dynamic spectral graph sparsifier implementing the ADKKP16 approach.
26///
27/// Maintains:
28/// - `g_full`: the full weighted graph (receives all updates)
29/// - `g_spec`: the compressed sparsifier (~O(n log n / eps^2) edges)
30/// - `backbone`: spanning forest guaranteeing connectivity
31/// - `scorer`: random-walk importance estimator
32/// - `auditor`: periodic spectral quality check
33///
34/// # Thread safety
35///
36/// The sparsifier wraps its state in [`RwLock`] internally. The public API
37/// takes `&mut self` to make ownership clear; concurrent readers can
38/// access the sparsifier graph via [`sparsifier`](Self::sparsifier) which
39/// clones the current snapshot.
40pub struct AdaptiveGeoSpar {
41    /// The full graph receiving all dynamic updates.
42    g_full: SparseGraph,
43    /// The compressed spectral sparsifier.
44    g_spec: SparseGraph,
45    /// Backbone spanning forest.
46    backbone: Backbone,
47    /// Edge importance scorer.
48    scorer: LocalImportanceScorer,
49    /// Spectral sampler.
50    sampler: SpectralSampler,
51    /// Spectral auditor.
52    auditor: SpectralAuditor,
53    /// Configuration.
54    config: SparsifierConfig,
55    /// Runtime statistics.
56    stats: SparsifierStats,
57    /// Set of backbone edge keys for the sampler.
58    backbone_edge_set: HashSet<(usize, usize)>,
59    /// Thread-safe snapshot for readers (updated after rebuilds).
60    snapshot: RwLock<SparseGraph>,
61    /// Cached total importance (avoids O(m) re-computation per insert).
62    cached_total_importance: f64,
63}
64
65impl std::fmt::Debug for AdaptiveGeoSpar {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        f.debug_struct("AdaptiveGeoSpar")
68            .field("vertices", &self.g_full.num_vertices())
69            .field("full_edges", &self.g_full.num_edges())
70            .field("spec_edges", &self.g_spec.num_edges())
71            .field("config", &self.config)
72            .field("stats", &self.stats)
73            .finish()
74    }
75}
76
77impl AdaptiveGeoSpar {
78    // ----- construction ----------------------------------------------------
79
80    /// Create a new empty sparsifier with the given configuration.
81    pub fn new(config: SparsifierConfig) -> Self {
82        let scorer = LocalImportanceScorer::new(config.walk_length, config.num_walks);
83        let sampler = SpectralSampler::new(config.epsilon);
84        let auditor = SpectralAuditor::new(config.n_audit_probes, config.epsilon);
85
86        Self {
87            g_full: SparseGraph::new(),
88            g_spec: SparseGraph::new(),
89            backbone: Backbone::new(0),
90            scorer,
91            sampler,
92            auditor,
93            config,
94            stats: SparsifierStats::default(),
95            backbone_edge_set: HashSet::new(),
96            snapshot: RwLock::new(SparseGraph::new()),
97            cached_total_importance: 0.0,
98        }
99    }
100
101    /// Build a sparsifier from an existing static graph.
102    ///
103    /// This is the primary entry point for initial construction. It scores
104    /// all edges, samples according to importance, and sets up the backbone.
105    pub fn build(graph: &SparseGraph, config: SparsifierConfig) -> Result<Self> {
106        if graph.num_vertices() == 0 {
107            return Err(SparsifierError::EmptyGraph);
108        }
109
110        let mut spar = Self::new(config);
111        spar.g_full = graph.clone();
112        spar.backbone = Backbone::new(graph.num_vertices());
113
114        // Insert all edges into backbone.
115        for (u, v, w) in graph.edges() {
116            let is_bb = spar.backbone.insert_edge(u, v, w);
117            if is_bb {
118                spar.backbone_edge_set.insert(Self::edge_key(u, v));
119            }
120        }
121
122        // Score and sample.
123        spar.do_full_rebuild()?;
124
125        info!(
126            vertices = graph.num_vertices(),
127            full_edges = graph.num_edges(),
128            spec_edges = spar.g_spec.num_edges(),
129            compression = %format!("{:.2}x", spar.compression_ratio()),
130            "Built initial sparsifier"
131        );
132
133        Ok(spar)
134    }
135
136    // ----- dynamic updates -------------------------------------------------
137
138    /// Handle the insertion of an edge into the full graph.
139    ///
140    /// The edge is added to `g_full`, the backbone is updated, and the
141    /// sparsifier is incrementally updated. Periodic audits may trigger
142    /// a local or full rebuild.
143    pub fn handle_insert(&mut self, u: usize, v: usize, weight: f64) -> Result<()> {
144        // Validate.
145        if !weight.is_finite() || weight <= 0.0 {
146            return Err(SparsifierError::InvalidWeight(weight));
147        }
148
149        // Insert into full graph.
150        self.g_full.insert_edge(u, v, weight)?;
151
152        // Update backbone.
153        let is_bb = self.backbone.insert_edge(u, v, weight);
154        if is_bb {
155            self.backbone_edge_set.insert(Self::edge_key(u, v));
156            // Backbone edges always go into the sparsifier.
157            let _ = self.g_spec.insert_or_update_edge(u, v, weight);
158        } else {
159            // Score and probabilistically add to sparsifier.
160            let importance = self.scorer.score(&self.g_full, u, v, weight);
161            let budget = self.edge_budget();
162            // Incrementally update cached total importance instead of O(m) recompute.
163            self.cached_total_importance += importance.score;
164            let total_imp = self.cached_total_importance.max(importance.score);
165
166            if let Some((su, sv, sw)) = self.sampler.sample_single_edge(
167                &importance,
168                self.g_full.num_vertices(),
169                total_imp,
170                budget,
171            ) {
172                let _ = self.g_spec.insert_or_update_edge(su, sv, sw);
173            }
174        }
175
176        self.stats.insertions += 1;
177        self.stats.updates_since_audit += 1;
178        self.refresh_stats();
179        self.maybe_audit();
180
181        Ok(())
182    }
183
184    /// Handle the deletion of an edge from the full graph.
185    pub fn handle_delete(&mut self, u: usize, v: usize) -> Result<()> {
186        // Delete from full graph.
187        let weight = self.g_full.delete_edge(u, v)?;
188
189        // Delete from sparsifier if present.
190        let _ = self.g_spec.delete_edge(u, v);
191
192        // Update backbone.
193        let key = Self::edge_key(u, v);
194        if self.backbone_edge_set.remove(&key) {
195            self.backbone.delete_edge(u, v, weight);
196        }
197
198        self.stats.deletions += 1;
199        self.stats.updates_since_audit += 1;
200        self.refresh_stats();
201        self.maybe_audit();
202
203        Ok(())
204    }
205
206    /// Handle a point-move operation: a node's neighbourhood changes.
207    ///
208    /// `old_neighbors` are edges to remove, `new_neighbors` are edges to add.
209    pub fn update_embedding(
210        &mut self,
211        node: usize,
212        old_neighbors: &[(usize, f64)],
213        new_neighbors: &[(usize, f64)],
214    ) -> Result<()> {
215        // Remove old edges.
216        for &(v, _) in old_neighbors {
217            let _ = self.handle_delete(node, v);
218        }
219        // Add new edges.
220        for &(v, w) in new_neighbors {
221            let _ = self.handle_insert(node, v, w);
222        }
223        Ok(())
224    }
225
226    // ----- audit -----------------------------------------------------------
227
228    /// Run a spectral audit comparing `g_spec` against `g_full`.
229    pub fn run_audit(&self) -> AuditResult {
230        self.auditor
231            .audit_quadratic_form(&self.g_full, &self.g_spec, self.config.n_audit_probes)
232    }
233
234    /// Check if an audit is due, and if so, run it and optionally rebuild.
235    fn maybe_audit(&mut self) {
236        if self.stats.updates_since_audit < self.config.audit_interval as u64 {
237            return;
238        }
239
240        let result = self.run_audit();
241        self.stats.audit_count += 1;
242        self.stats.updates_since_audit = 0;
243
244        if result.passed {
245            self.stats.audit_pass_count += 1;
246            debug!(
247                max_error = result.max_error,
248                avg_error = result.avg_error,
249                "Spectral audit passed"
250            );
251        } else {
252            warn!(
253                max_error = result.max_error,
254                threshold = result.threshold,
255                "Spectral audit failed"
256            );
257            if self.config.auto_rebuild_on_audit_failure {
258                let _ = self.do_full_rebuild();
259            }
260        }
261    }
262
263    // ----- rebuild ---------------------------------------------------------
264
265    /// Rebuild the sparsifier around specific vertices.
266    ///
267    /// Re-scores and re-samples edges incident to the given nodes.
268    pub fn do_local_rebuild(&mut self, nodes: &[usize]) -> Result<()> {
269        debug!(n_nodes = nodes.len(), "Local rebuild");
270
271        // Collect edges incident to the target nodes.
272        let node_set: HashSet<usize> = nodes.iter().copied().collect();
273        let incident_edges: Vec<(usize, usize, f64)> = self
274            .g_full
275            .edges()
276            .filter(|(u, v, _)| node_set.contains(u) || node_set.contains(v))
277            .collect();
278
279        // Remove these edges from the sparsifier.
280        for &(u, v, _) in &incident_edges {
281            let _ = self.g_spec.delete_edge(u, v);
282        }
283
284        // Re-score and re-sample.
285        let scores: Vec<_> = incident_edges
286            .iter()
287            .map(|&(u, v, w)| self.scorer.score(&self.g_full, u, v, w))
288            .collect();
289
290        let budget = self.edge_budget();
291        let sampled = self
292            .sampler
293            .sample_edges(&scores, budget, &self.backbone_edge_set);
294
295        // Merge sampled edges back.
296        for (u, v, w) in sampled.edges() {
297            let _ = self.g_spec.insert_or_update_edge(u, v, w);
298        }
299
300        self.stats.local_rebuilds += 1;
301        self.refresh_stats();
302        self.update_snapshot();
303
304        Ok(())
305    }
306
307    /// Full reconstruction of the sparsifier from scratch.
308    fn do_full_rebuild(&mut self) -> Result<()> {
309        debug!("Full sparsifier rebuild");
310
311        let scores = self.scorer.score_all(&self.g_full);
312        // Refresh cached total importance from fresh scores.
313        self.cached_total_importance = scores.iter().map(|s| s.score).sum();
314        let budget = self.edge_budget();
315        self.g_spec = self
316            .sampler
317            .sample_edges(&scores, budget, &self.backbone_edge_set);
318
319        self.stats.full_rebuilds += 1;
320        self.refresh_stats();
321        self.update_snapshot();
322
323        Ok(())
324    }
325
326    // ----- accessors -------------------------------------------------------
327
328    /// Get a reference to the full graph.
329    pub fn full_graph(&self) -> &SparseGraph {
330        &self.g_full
331    }
332
333    /// Get a reference to the current sparsifier graph.
334    pub fn sparsifier_graph(&self) -> &SparseGraph {
335        &self.g_spec
336    }
337
338    /// Get a clone of the thread-safe sparsifier snapshot.
339    pub fn snapshot(&self) -> SparseGraph {
340        self.snapshot.read().clone()
341    }
342
343    /// Get the current configuration.
344    pub fn config(&self) -> &SparsifierConfig {
345        &self.config
346    }
347
348    // ----- helpers ---------------------------------------------------------
349
350    /// Target edge budget for the sparsifier.
351    fn edge_budget(&self) -> usize {
352        self.config.edge_budget_factor * self.g_full.num_vertices().max(1)
353    }
354
355    /// Canonical edge key.
356    fn edge_key(u: usize, v: usize) -> (usize, usize) {
357        if u <= v { (u, v) } else { (v, u) }
358    }
359
360    /// Refresh derived statistics.
361    fn refresh_stats(&mut self) {
362        self.stats.vertex_count = self.g_full.num_vertices();
363        self.stats.full_edge_count = self.g_full.num_edges();
364        self.stats.edge_count = self.g_spec.num_edges();
365        self.stats.refresh_ratio();
366    }
367
368    /// Push the current `g_spec` into the thread-safe snapshot.
369    fn update_snapshot(&self) {
370        let mut snap = self.snapshot.write();
371        *snap = self.g_spec.clone();
372    }
373}
374
375// ---------------------------------------------------------------------------
376// Trait implementation
377// ---------------------------------------------------------------------------
378
379impl Sparsifier for AdaptiveGeoSpar {
380    fn insert_edge(&mut self, u: usize, v: usize, weight: f64) -> Result<()> {
381        self.handle_insert(u, v, weight)
382    }
383
384    fn delete_edge(&mut self, u: usize, v: usize) -> Result<()> {
385        self.handle_delete(u, v)
386    }
387
388    fn audit(&self) -> AuditResult {
389        self.run_audit()
390    }
391
392    fn rebuild_local(&mut self, nodes: &[usize]) -> Result<()> {
393        self.do_local_rebuild(nodes)
394    }
395
396    fn rebuild_full(&mut self) -> Result<()> {
397        self.do_full_rebuild()
398    }
399
400    fn sparsifier(&self) -> &SparseGraph {
401        &self.g_spec
402    }
403
404    fn compression_ratio(&self) -> f64 {
405        if self.g_spec.num_edges() == 0 {
406            return 0.0;
407        }
408        self.g_full.num_edges() as f64 / self.g_spec.num_edges() as f64
409    }
410
411    fn stats(&self) -> &SparsifierStats {
412        &self.stats
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    fn triangle_graph() -> SparseGraph {
421        SparseGraph::from_edges(&[
422            (0, 1, 1.0),
423            (1, 2, 1.0),
424            (2, 0, 1.0),
425        ])
426    }
427
428    fn path_graph(n: usize) -> SparseGraph {
429        let edges: Vec<_> = (0..n - 1).map(|i| (i, i + 1, 1.0)).collect();
430        SparseGraph::from_edges(&edges)
431    }
432
433    #[test]
434    fn test_build_triangle() {
435        let g = triangle_graph();
436        let config = SparsifierConfig::default();
437        let spar = AdaptiveGeoSpar::build(&g, config).unwrap();
438
439        assert_eq!(spar.full_graph().num_vertices(), 3);
440        assert_eq!(spar.full_graph().num_edges(), 3);
441        assert!(spar.sparsifier_graph().num_edges() > 0);
442        assert!(spar.compression_ratio() > 0.0);
443    }
444
445    #[test]
446    fn test_build_path() {
447        let g = path_graph(10);
448        let config = SparsifierConfig::default();
449        let spar = AdaptiveGeoSpar::build(&g, config).unwrap();
450
451        // A path has n-1 edges; the sparsifier should keep most/all of them
452        // since they are all bridges.
453        assert!(spar.sparsifier_graph().num_edges() >= 5);
454    }
455
456    #[test]
457    fn test_dynamic_insert() {
458        let g = triangle_graph();
459        let config = SparsifierConfig::default();
460        let mut spar = AdaptiveGeoSpar::build(&g, config).unwrap();
461
462        spar.handle_insert(0, 3, 2.0).unwrap();
463        assert_eq!(spar.full_graph().num_edges(), 4);
464        assert_eq!(spar.stats().insertions, 1);
465    }
466
467    #[test]
468    fn test_dynamic_delete() {
469        let g = triangle_graph();
470        let config = SparsifierConfig::default();
471        let mut spar = AdaptiveGeoSpar::build(&g, config).unwrap();
472
473        spar.handle_delete(0, 1).unwrap();
474        assert_eq!(spar.full_graph().num_edges(), 2);
475        assert_eq!(spar.stats().deletions, 1);
476    }
477
478    #[test]
479    fn test_audit_passes_on_identical() {
480        let g = triangle_graph();
481        let mut config = SparsifierConfig::default();
482        config.epsilon = 0.5; // generous threshold
483        let spar = AdaptiveGeoSpar::build(&g, config).unwrap();
484
485        let result = spar.run_audit();
486        // For a tiny graph the sparsifier should be very close.
487        assert!(result.avg_error < 1.0);
488    }
489
490    #[test]
491    fn test_empty_graph_error() {
492        let g = SparseGraph::new();
493        let config = SparsifierConfig::default();
494        let result = AdaptiveGeoSpar::build(&g, config);
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn test_update_embedding() {
500        let g = triangle_graph();
501        let config = SparsifierConfig::default();
502        let mut spar = AdaptiveGeoSpar::build(&g, config).unwrap();
503
504        // Move vertex 0: remove edge to 1, add edge to new vertex 3.
505        spar.update_embedding(
506            0,
507            &[(1, 1.0)],
508            &[(3, 2.0)],
509        )
510        .unwrap();
511
512        assert!(!spar.full_graph().has_edge(0, 1));
513        assert!(spar.full_graph().has_edge(0, 3));
514    }
515
516    #[test]
517    fn test_rebuild_full() {
518        let g = path_graph(5);
519        let config = SparsifierConfig::default();
520        let mut spar = AdaptiveGeoSpar::build(&g, config).unwrap();
521
522        spar.rebuild_full().unwrap();
523        assert_eq!(spar.stats().full_rebuilds, 2); // initial build + explicit
524        assert!(spar.sparsifier_graph().num_edges() > 0);
525    }
526
527    #[test]
528    fn test_stats_tracking() {
529        let g = triangle_graph();
530        let config = SparsifierConfig::default();
531        let spar = AdaptiveGeoSpar::build(&g, config).unwrap();
532
533        let stats = spar.stats();
534        assert_eq!(stats.vertex_count, 3);
535        assert_eq!(stats.full_edge_count, 3);
536        assert!(stats.edge_count > 0);
537    }
538}