1use 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
21pub struct AdaptiveGeoSpar {
41 g_full: SparseGraph,
43 g_spec: SparseGraph,
45 backbone: Backbone,
47 scorer: LocalImportanceScorer,
49 sampler: SpectralSampler,
51 auditor: SpectralAuditor,
53 config: SparsifierConfig,
55 stats: SparsifierStats,
57 backbone_edge_set: HashSet<(usize, usize)>,
59 snapshot: RwLock<SparseGraph>,
61 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 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 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 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 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 pub fn handle_insert(&mut self, u: usize, v: usize, weight: f64) -> Result<()> {
144 if !weight.is_finite() || weight <= 0.0 {
146 return Err(SparsifierError::InvalidWeight(weight));
147 }
148
149 self.g_full.insert_edge(u, v, weight)?;
151
152 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 let _ = self.g_spec.insert_or_update_edge(u, v, weight);
158 } else {
159 let importance = self.scorer.score(&self.g_full, u, v, weight);
161 let budget = self.edge_budget();
162 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 pub fn handle_delete(&mut self, u: usize, v: usize) -> Result<()> {
186 let weight = self.g_full.delete_edge(u, v)?;
188
189 let _ = self.g_spec.delete_edge(u, v);
191
192 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 pub fn update_embedding(
210 &mut self,
211 node: usize,
212 old_neighbors: &[(usize, f64)],
213 new_neighbors: &[(usize, f64)],
214 ) -> Result<()> {
215 for &(v, _) in old_neighbors {
217 let _ = self.handle_delete(node, v);
218 }
219 for &(v, w) in new_neighbors {
221 let _ = self.handle_insert(node, v, w);
222 }
223 Ok(())
224 }
225
226 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 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 pub fn do_local_rebuild(&mut self, nodes: &[usize]) -> Result<()> {
269 debug!(n_nodes = nodes.len(), "Local rebuild");
270
271 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 for &(u, v, _) in &incident_edges {
281 let _ = self.g_spec.delete_edge(u, v);
282 }
283
284 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 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 fn do_full_rebuild(&mut self) -> Result<()> {
309 debug!("Full sparsifier rebuild");
310
311 let scores = self.scorer.score_all(&self.g_full);
312 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 pub fn full_graph(&self) -> &SparseGraph {
330 &self.g_full
331 }
332
333 pub fn sparsifier_graph(&self) -> &SparseGraph {
335 &self.g_spec
336 }
337
338 pub fn snapshot(&self) -> SparseGraph {
340 self.snapshot.read().clone()
341 }
342
343 pub fn config(&self) -> &SparsifierConfig {
345 &self.config
346 }
347
348 fn edge_budget(&self) -> usize {
352 self.config.edge_budget_factor * self.g_full.num_vertices().max(1)
353 }
354
355 fn edge_key(u: usize, v: usize) -> (usize, usize) {
357 if u <= v { (u, v) } else { (v, u) }
358 }
359
360 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 fn update_snapshot(&self) {
370 let mut snap = self.snapshot.write();
371 *snap = self.g_spec.clone();
372 }
373}
374
375impl 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 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; let spar = AdaptiveGeoSpar::build(&g, config).unwrap();
484
485 let result = spar.run_audit();
486 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 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); 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}