1use crate::error::{M1ndError, M1ndResult};
4use crate::graph::Graph;
5use crate::types::*;
6
7#[derive(Clone, Debug)]
15pub struct CommunityResult {
16 pub assignments: Vec<CommunityId>,
18 pub num_communities: u32,
20 pub modularity: FiniteF32,
22 pub passes: u32,
24}
25
26#[derive(Clone, Debug)]
28pub struct CommunityStats {
29 pub id: CommunityId,
30 pub node_count: u32,
31 pub internal_edges: u32,
32 pub external_edges: u32,
33 pub density: FiniteF32,
35}
36
37pub struct CommunityDetector {
40 max_passes: u32,
41 min_modularity_gain: FiniteF32,
42}
43
44impl CommunityDetector {
45 pub fn new(max_passes: u32, min_modularity_gain: FiniteF32) -> Self {
46 Self {
47 max_passes,
48 min_modularity_gain,
49 }
50 }
51
52 pub fn with_defaults() -> Self {
53 Self {
54 max_passes: 20,
55 min_modularity_gain: FiniteF32::new(1e-6),
56 }
57 }
58
59 pub fn detect(&self, graph: &Graph) -> M1ndResult<CommunityResult> {
65 let n = graph.num_nodes() as usize;
66 if n == 0 {
67 return Err(M1ndError::EmptyGraph);
68 }
69
70 let mut community: Vec<u32> = (0..n as u32).collect();
72
73 let mut adj: Vec<std::collections::HashMap<u32, f64>> =
78 vec![std::collections::HashMap::new(); n];
79 let mut seen = std::collections::HashSet::new();
80
81 for i in 0..n {
82 let range = graph.csr.out_range(NodeId::new(i as u32));
83 for j in range {
84 let target = graph.csr.targets[j].as_usize();
85 let w = graph.csr.read_weight(EdgeIdx::new(j as u32)).get() as f64;
86
87 let (lo, hi) = if i <= target {
88 (i, target)
89 } else {
90 (target, i)
91 };
92 if !seen.insert((lo, hi)) {
93 continue; }
95
96 *adj[i].entry(target as u32).or_default() += w;
97 if i != target {
98 *adj[target].entry(i as u32).or_default() += w;
99 } else {
100 *adj[i].entry(i as u32).or_default() += w;
102 }
103 }
104 }
105
106 let mut degree = vec![0.0f64; n];
108 let mut two_m = 0.0f64;
109 for i in 0..n {
110 degree[i] = adj[i].values().sum();
111 two_m += degree[i];
112 }
113
114 if two_m <= 0.0 {
115 return Ok(CommunityResult {
116 assignments: community.iter().map(|&c| CommunityId(c)).collect(),
117 num_communities: n as u32,
118 modularity: FiniteF32::ZERO,
119 passes: 0,
120 });
121 }
122
123 let mut sum_in = vec![0.0f64; n];
125 let mut sum_tot: Vec<f64> = degree.clone();
126
127 let mut converged = false;
128 let mut pass = 0u32;
129
130 while pass < self.max_passes {
131 pass += 1;
132 let mut improved = false;
133
134 for i in 0..n {
135 let ki = degree[i];
136 let ci = community[i] as usize;
137
138 let mut ki_in_old = 0.0f64;
140 for (&nb, &w) in &adj[i] {
141 if community[nb as usize] as usize == ci {
142 ki_in_old += w;
143 }
144 }
145
146 sum_in[ci] -= 2.0 * ki_in_old;
147 sum_tot[ci] -= ki;
148
149 let mut best_comm = ci;
151 let mut best_delta_q = 0.0f64;
152
153 let mut neighbor_comms: std::collections::HashMap<usize, f64> =
154 std::collections::HashMap::new();
155 for (&nb, &w) in &adj[i] {
156 let cj = community[nb as usize] as usize;
157 *neighbor_comms.entry(cj).or_insert(0.0) += w;
158 }
159
160 for (&cj, &ki_in_new) in &neighbor_comms {
161 let delta_q = (ki_in_new / two_m) - (sum_tot[cj] * ki / (two_m * two_m));
162 let delta_old = (ki_in_old / two_m) - (sum_tot[ci] * ki / (two_m * two_m));
163 let gain = delta_q - delta_old;
164 if gain > best_delta_q {
165 best_delta_q = gain;
166 best_comm = cj;
167 }
168 }
169
170 community[i] = best_comm as u32;
171
172 let mut ki_in_new_actual = 0.0f64;
174 for (&nb, &w) in &adj[i] {
175 if community[nb as usize] as usize == best_comm {
176 ki_in_new_actual += w;
177 }
178 }
179
180 sum_in[best_comm] += 2.0 * ki_in_new_actual;
181 sum_tot[best_comm] += ki;
182
183 if best_comm != ci {
184 improved = true;
185 }
186 }
187
188 if !improved {
189 converged = true;
190 break;
191 }
192 }
193
194 if !converged && pass >= self.max_passes {
195 }
197
198 let mut comm_map = std::collections::HashMap::new();
200 let mut next_id = 0u32;
201 let assignments: Vec<CommunityId> = community
202 .iter()
203 .map(|&c| {
204 let id = comm_map.entry(c).or_insert_with(|| {
205 let id = next_id;
206 next_id += 1;
207 id
208 });
209 CommunityId(*id)
210 })
211 .collect();
212
213 let mut q = 0.0f64;
215 for i in 0..n {
216 for (&nb, &w) in &adj[i] {
217 let j = nb as usize;
218 if assignments[i] == assignments[j] {
219 q += w - degree[i] * degree[j] / two_m;
220 }
221 }
222 }
223 q /= two_m;
224
225 Ok(CommunityResult {
226 assignments,
227 num_communities: next_id,
228 modularity: FiniteF32::new(q as f32),
229 passes: pass,
230 })
231 }
232
233 pub fn community_stats(graph: &Graph, result: &CommunityResult) -> Vec<CommunityStats> {
236 let n = graph.num_nodes() as usize;
237 let num_comm = result.num_communities as usize;
238 let mut node_counts = vec![0u32; num_comm];
239 let mut internal = vec![0u32; num_comm];
240 let mut external = vec![0u32; num_comm];
241
242 for i in 0..n {
243 let ci = result.assignments[i].0 as usize;
244 node_counts[ci] += 1;
245
246 let range = graph.csr.out_range(NodeId::new(i as u32));
247 for j in range {
248 let tgt = graph.csr.targets[j].as_usize();
249 if tgt < n {
250 if result.assignments[tgt].0 as usize == ci {
251 internal[ci] += 1;
252 } else {
253 external[ci] += 1;
254 }
255 }
256 }
257 }
258
259 (0..num_comm)
260 .map(|c| {
261 let nc = node_counts[c];
262 let max_internal = if nc > 1 { nc * (nc - 1) } else { 1 };
263 let density = if max_internal > 0 {
264 internal[c] as f32 / max_internal as f32
265 } else {
266 0.0
267 };
268 CommunityStats {
269 id: CommunityId(c as u32),
270 node_count: nc,
271 internal_edges: internal[c],
272 external_edges: external[c],
273 density: FiniteF32::new(density.min(1.0)),
274 }
275 })
276 .collect()
277 }
278}
279
280#[derive(Clone, Debug)]
288pub struct Bridge {
289 pub source: NodeId,
290 pub target: NodeId,
291 pub edge_idx: EdgeIdx,
292 pub source_community: CommunityId,
293 pub target_community: CommunityId,
294 pub importance: FiniteF32,
296}
297
298pub struct BridgeDetector;
302
303impl BridgeDetector {
304 pub fn detect(graph: &Graph, communities: &CommunityResult) -> M1ndResult<Vec<Bridge>> {
307 let n = graph.num_nodes() as usize;
308 let mut bridges = Vec::new();
309
310 for i in 0..n {
311 let ci = communities.assignments[i];
312 let range = graph.csr.out_range(NodeId::new(i as u32));
313 for j in range {
314 let tgt = graph.csr.targets[j];
315 let tgt_idx = tgt.as_usize();
316 if tgt_idx >= n {
317 continue;
318 }
319 let cj = communities.assignments[tgt_idx];
320 if ci != cj {
321 let w = graph.csr.read_weight(EdgeIdx::new(j as u32)).get();
322 bridges.push(Bridge {
323 source: NodeId::new(i as u32),
324 target: tgt,
325 edge_idx: EdgeIdx::new(j as u32),
326 source_community: ci,
327 target_community: cj,
328 importance: FiniteF32::new(w),
329 });
330 }
331 }
332 }
333
334 bridges.sort_by(|a, b| b.importance.cmp(&a.importance));
335 Ok(bridges)
336 }
337
338 pub fn detect_between(
340 graph: &Graph,
341 communities: &CommunityResult,
342 comm_a: CommunityId,
343 comm_b: CommunityId,
344 ) -> M1ndResult<Vec<Bridge>> {
345 let all = Self::detect(graph, communities)?;
346 Ok(all
347 .into_iter()
348 .filter(|b| {
349 (b.source_community == comm_a && b.target_community == comm_b)
350 || (b.source_community == comm_b && b.target_community == comm_a)
351 })
352 .collect())
353 }
354}
355
356#[derive(Clone, Debug)]
366pub struct SpectralGapResult {
367 pub algebraic_connectivity: FiniteF32,
369 pub spectral_gap: FiniteF32,
371 pub num_components: u32,
373 pub robustness: RobustnessLevel,
375 pub trials: u32,
377 pub converged: bool,
379}
380
381#[derive(Clone, Copy, Debug, PartialEq, Eq)]
382pub enum RobustnessLevel {
383 Fragile,
384 Moderate,
385 Robust,
386}
387
388pub struct SpectralGapAnalyzer {
391 max_iterations: u32,
392 tolerance: f64,
393 num_trials: u32,
394}
395
396impl SpectralGapAnalyzer {
397 pub fn new(max_iterations: u32, tolerance: f64, num_trials: u32) -> Self {
398 Self {
399 max_iterations,
400 tolerance,
401 num_trials,
402 }
403 }
404
405 pub fn with_defaults() -> Self {
406 Self {
407 max_iterations: 1000,
408 tolerance: 1e-8,
409 num_trials: 3,
410 }
411 }
412
413 pub fn analyze(&self, graph: &Graph) -> M1ndResult<SpectralGapResult> {
418 let n = graph.num_nodes() as usize;
419 if n == 0 {
420 return Err(M1ndError::EmptyGraph);
421 }
422
423 let mut component = vec![u32::MAX; n];
425 let mut num_components = 0u32;
426 for start in 0..n {
427 if component[start] != u32::MAX {
428 continue;
429 }
430 let mut queue = std::collections::VecDeque::new();
431 queue.push_back(start);
432 component[start] = num_components;
433 while let Some(node) = queue.pop_front() {
434 let range = graph.csr.out_range(NodeId::new(node as u32));
435 for j in range {
436 let tgt = graph.csr.targets[j].as_usize();
437 if tgt < n && component[tgt] == u32::MAX {
438 component[tgt] = num_components;
439 queue.push_back(tgt);
440 }
441 }
442 }
443 num_components += 1;
444 }
445
446 if n == 1 {
448 return Ok(SpectralGapResult {
449 algebraic_connectivity: FiniteF32::ZERO,
450 spectral_gap: FiniteF32::ZERO,
451 num_components,
452 robustness: RobustnessLevel::Fragile,
453 trials: 0,
454 converged: true,
455 });
456 }
457
458 let mut degree = vec![0.0f64; n];
466 for i in 0..n {
467 let range = graph.csr.out_range(NodeId::new(i as u32));
468 for j in range {
469 degree[i] += graph.csr.read_weight(EdgeIdx::new(j as u32)).get() as f64;
470 }
471 }
472
473 let max_eig_est = degree.iter().cloned().fold(0.0f64, f64::max) * 2.1;
475 if max_eig_est <= 0.0 {
476 return Ok(SpectralGapResult {
477 algebraic_connectivity: FiniteF32::ZERO,
478 spectral_gap: FiniteF32::ZERO,
479 num_components,
480 robustness: RobustnessLevel::Fragile,
481 trials: 0,
482 converged: true,
483 });
484 }
485
486 let nf = n as f64;
488 let mut best_lambda2 = f64::MAX;
489 let mut converged = false;
490
491 for trial in 0..self.num_trials {
493 let mut v: Vec<f64> = (0..n)
495 .map(|i| {
496 let seed = (i as u64).wrapping_mul(2654435761 + trial as u64 * 1000003);
497 (seed as f64 / u64::MAX as f64) - 0.5
498 })
499 .collect();
500
501 let avg: f64 = v.iter().sum::<f64>() / nf;
503 for x in &mut v {
504 *x -= avg;
505 }
506
507 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
509 if norm < 1e-15 {
510 continue;
511 }
512 for x in &mut v {
513 *x /= norm;
514 }
515
516 let mut eigenvalue = 0.0f64;
517
518 for _iter in 0..self.max_iterations {
519 let mut w = vec![0.0f64; n];
522 for i in 0..n {
523 w[i] = (max_eig_est - degree[i]) * v[i];
525 let range = graph.csr.out_range(NodeId::new(i as u32));
527 for j in range {
528 let tgt = graph.csr.targets[j].as_usize();
529 let wgt = graph.csr.read_weight(EdgeIdx::new(j as u32)).get() as f64;
530 w[i] += wgt * v[tgt];
531 }
532 }
533
534 let avg_w: f64 = w.iter().sum::<f64>() / nf;
536 for x in &mut w {
537 *x -= avg_w;
538 }
539
540 let dot: f64 = w.iter().zip(v.iter()).map(|(a, b)| a * b).sum();
542 let new_norm: f64 = w.iter().map(|x| x * x).sum::<f64>().sqrt();
543
544 if new_norm < 1e-15 {
545 break;
546 }
547
548 eigenvalue = dot;
549
550 let residual: f64 = w
552 .iter()
553 .zip(v.iter())
554 .map(|(wi, vi)| (wi / new_norm - vi).powi(2))
555 .sum::<f64>()
556 .sqrt();
557
558 for i in 0..n {
560 v[i] = w[i] / new_norm;
561 }
562
563 if residual < self.tolerance {
564 converged = true;
565 break;
566 }
567 }
568
569 let lambda2 = (max_eig_est - eigenvalue).max(0.0);
571 if lambda2 < best_lambda2 {
572 best_lambda2 = lambda2;
573 }
574 }
575
576 let algebraic_connectivity = best_lambda2.min(100.0) as f32;
577 let spectral_gap = if max_eig_est > 0.0 {
578 (algebraic_connectivity as f64 / max_eig_est) as f32
579 } else {
580 0.0
581 };
582
583 let robustness = if algebraic_connectivity < 0.01 {
584 RobustnessLevel::Fragile
585 } else if algebraic_connectivity < 0.5 {
586 RobustnessLevel::Moderate
587 } else {
588 RobustnessLevel::Robust
589 };
590
591 Ok(SpectralGapResult {
592 algebraic_connectivity: FiniteF32::new(algebraic_connectivity),
593 spectral_gap: FiniteF32::new(spectral_gap),
594 num_components,
595 robustness,
596 trials: self.num_trials,
597 converged,
598 })
599 }
600}
601
602#[derive(Clone, Debug)]
609pub struct Fingerprint {
610 pub node: NodeId,
611 pub responses: Vec<FiniteF32>,
613 pub lsh_hash: u64,
615}
616
617#[derive(Clone, Debug)]
619pub struct EquivalentPair {
620 pub node_a: NodeId,
621 pub node_b: NodeId,
622 pub cosine_similarity: FiniteF32,
623 pub directly_connected: bool,
624}
625
626pub struct ActivationFingerprinter {
629 pair_budget: u64,
631 similarity_threshold: FiniteF32,
633}
634
635impl ActivationFingerprinter {
636 pub fn new(pair_budget: u64, similarity_threshold: FiniteF32) -> Self {
637 Self {
638 pair_budget,
639 similarity_threshold,
640 }
641 }
642
643 pub fn compute_fingerprints(
646 &self,
647 graph: &Graph,
648 engine: &crate::activation::HybridEngine,
649 probe_queries: &[Vec<(NodeId, FiniteF32)>],
650 ) -> M1ndResult<Vec<Fingerprint>> {
651 use crate::activation::ActivationEngine;
652
653 let n = graph.num_nodes() as usize;
654 let config = PropagationConfig::default();
655 let num_probes = probe_queries.len();
656
657 let mut responses = vec![vec![FiniteF32::ZERO; num_probes]; n];
659
660 for (pi, seeds) in probe_queries.iter().enumerate() {
661 let result = engine.propagate(graph, seeds, &config)?;
662 for &(node, score) in &result.scores {
663 let idx = node.as_usize();
664 if idx < n {
665 responses[idx][pi] = score;
666 }
667 }
668 }
669
670 let fingerprints: Vec<Fingerprint> = (0..n)
672 .map(|i| {
673 let resp = &responses[i];
674 let mut hash = 0u64;
676 for (pi, &v) in resp.iter().enumerate() {
677 if v.get() > 0.0 {
678 hash |= 1u64 << (pi & 63);
679 }
680 }
681 Fingerprint {
682 node: NodeId::new(i as u32),
683 responses: resp.clone(),
684 lsh_hash: hash,
685 }
686 })
687 .collect();
688
689 Ok(fingerprints)
690 }
691
692 pub fn find_equivalents(
696 &self,
697 fingerprints: &[Fingerprint],
698 graph: &Graph,
699 ) -> M1ndResult<Vec<EquivalentPair>> {
700 let mut buckets: std::collections::HashMap<u64, Vec<usize>> =
702 std::collections::HashMap::new();
703 for (i, fp) in fingerprints.iter().enumerate() {
704 buckets.entry(fp.lsh_hash).or_default().push(i);
705 }
706
707 let mut pairs = Vec::new();
708 let mut pair_count = 0u64;
709
710 for bucket in buckets.values() {
711 if bucket.len() < 2 {
712 continue;
713 }
714 for a_idx in 0..bucket.len() {
715 for b_idx in (a_idx + 1)..bucket.len() {
716 if pair_count >= self.pair_budget {
717 break;
718 }
719
720 let a = &fingerprints[bucket[a_idx]];
721 let b = &fingerprints[bucket[b_idx]];
722
723 let sim = Self::cosine_sim(&a.responses, &b.responses);
724 if sim >= self.similarity_threshold.get() {
725 let connected = {
727 let range = graph.csr.out_range(a.node);
728 range.into_iter().any(|j| graph.csr.targets[j] == b.node)
729 };
730
731 pairs.push(EquivalentPair {
732 node_a: a.node,
733 node_b: b.node,
734 cosine_similarity: FiniteF32::new(sim),
735 directly_connected: connected,
736 });
737 }
738 pair_count += 1;
739 }
740 }
741 }
742
743 pairs.sort_by(|a, b| b.cosine_similarity.cmp(&a.cosine_similarity));
744 Ok(pairs)
745 }
746
747 pub fn find_equivalents_of(
749 &self,
750 target: NodeId,
751 fingerprints: &[Fingerprint],
752 graph: &Graph,
753 ) -> M1ndResult<Vec<EquivalentPair>> {
754 let target_idx = target.as_usize();
755 if target_idx >= fingerprints.len() {
756 return Ok(Vec::new());
757 }
758
759 let target_fp = &fingerprints[target_idx];
760 let mut pairs = Vec::new();
761
762 for (i, fp) in fingerprints.iter().enumerate() {
763 if i == target_idx {
764 continue;
765 }
766 let sim = Self::cosine_sim(&target_fp.responses, &fp.responses);
767 if sim >= self.similarity_threshold.get() {
768 let connected = {
769 let range = graph.csr.out_range(target);
770 range.into_iter().any(|j| graph.csr.targets[j] == fp.node)
771 };
772 pairs.push(EquivalentPair {
773 node_a: target,
774 node_b: fp.node,
775 cosine_similarity: FiniteF32::new(sim),
776 directly_connected: connected,
777 });
778 }
779 }
780
781 pairs.sort_by(|a, b| b.cosine_similarity.cmp(&a.cosine_similarity));
782 Ok(pairs)
783 }
784
785 fn cosine_sim(a: &[FiniteF32], b: &[FiniteF32]) -> f32 {
786 let mut dot = 0.0f32;
787 let mut na = 0.0f32;
788 let mut nb = 0.0f32;
789 for i in 0..a.len().min(b.len()) {
790 dot += a[i].get() * b[i].get();
791 na += a[i].get() * a[i].get();
792 nb += b[i].get() * b[i].get();
793 }
794 let denom = na.sqrt() * nb.sqrt();
795 if denom > 0.0 {
796 (dot / denom).min(1.0)
797 } else {
798 0.0
799 }
800 }
801}
802
803#[derive(Clone, Debug)]
810pub struct ScaleView {
811 pub scale: u8,
812 pub communities: CommunityResult,
813 pub bridges: Vec<Bridge>,
814}
815
816pub struct MultiScaleViewer;
819
820impl MultiScaleViewer {
821 pub fn compute(graph: &Graph, max_scales: u8) -> M1ndResult<Vec<ScaleView>> {
824 let detector = CommunityDetector::with_defaults();
826 let communities = detector.detect(graph)?;
827 let bridges = BridgeDetector::detect(graph, &communities)?;
828
829 Ok(vec![ScaleView {
830 scale: 0,
831 communities,
832 bridges,
833 }])
834 }
835}
836
837#[derive(Clone, Debug)]
844pub struct TopologyReport {
845 pub communities: CommunityResult,
846 pub community_stats: Vec<CommunityStats>,
847 pub bridges: Vec<Bridge>,
848 pub spectral: SpectralGapResult,
849}
850
851pub struct TopologyAnalyzer {
854 pub community_detector: CommunityDetector,
855 pub spectral_analyzer: SpectralGapAnalyzer,
856 pub fingerprinter: ActivationFingerprinter,
857}
858
859impl TopologyAnalyzer {
860 pub fn with_defaults() -> Self {
861 Self {
862 community_detector: CommunityDetector::with_defaults(),
863 spectral_analyzer: SpectralGapAnalyzer::with_defaults(),
864 fingerprinter: ActivationFingerprinter::new(100_000, FiniteF32::new(0.85)),
865 }
866 }
867
868 pub fn analyze(&self, graph: &Graph) -> M1ndResult<TopologyReport> {
872 let communities = self.community_detector.detect(graph)?;
873 let community_stats = CommunityDetector::community_stats(graph, &communities);
874 let bridges = BridgeDetector::detect(graph, &communities)?;
875 let spectral = self.spectral_analyzer.analyze(graph)?;
876
877 Ok(TopologyReport {
878 communities,
879 community_stats,
880 bridges,
881 spectral,
882 })
883 }
884}
885
886static_assertions::assert_impl_all!(TopologyAnalyzer: Send, Sync);