1use super::simplicial::SimplicialComplex;
18use crate::base::Graph;
19use crate::error::{GraphError, Result};
20use scirs2_core::ndarray::{Array2, Array3};
21use std::collections::HashMap;
22
23pub struct MotifTensor {
34 pub data: Array3<f64>,
36 pub n: usize,
38}
39
40impl MotifTensor {
41 pub fn from_adjacency(adj: &Array2<f64>) -> Self {
52 let n = adj.nrows();
53 let mut data = Array3::<f64>::zeros((n, n, n));
54 for i in 0..n {
55 for j in 0..n {
56 if adj[[i, j]] == 0.0 {
57 continue;
58 }
59 for k in 0..n {
60 if i == j || j == k || i == k {
61 continue;
62 }
63 let val = adj[[i, j]] * adj[[j, k]] * adj[[i, k]];
64 if val != 0.0 {
65 data[[i, j, k]] = val;
66 }
67 }
68 }
69 }
70 MotifTensor { data, n }
71 }
72
73 pub fn triangle_count(&self) -> f64 {
78 self.data.iter().sum::<f64>() / 6.0
79 }
80
81 pub fn tensor_vec_product(&self, x: &[f64]) -> Vec<f64> {
88 let n = self.n;
89 let mut result = vec![0.0f64; n];
90 for i in 0..n {
91 for j in 0..n {
92 for k in 0..n {
93 result[i] += self.data[[i, j, k]] * x[j] * x[k];
94 }
95 }
96 }
97 result
98 }
99
100 pub fn z_eigenvector_centrality(&self) -> Result<(f64, Vec<f64>)> {
109 let n = self.n;
110 if n == 0 {
111 return Err(GraphError::InvalidGraph("empty motif tensor".to_string()));
112 }
113
114 let mut x = vec![1.0 / (n as f64).sqrt(); n];
116 let max_iter = 2000;
117 let tol = 1e-9;
118
119 for _ in 0..max_iter {
120 let mut tx2 = self.tensor_vec_product(&x);
121 let lambda: f64 = tx2.iter().zip(x.iter()).map(|(a, b)| a * b).sum();
123 for v in &mut tx2 {
125 if *v < 0.0 {
126 *v = 0.0;
127 }
128 }
129 let norm: f64 = tx2.iter().map(|v| v * v).sum::<f64>().sqrt();
130 if norm < 1e-15 {
131 return Ok((0.0, vec![0.0; n]));
132 }
133 let x_new: Vec<f64> = tx2.iter().map(|v| v / norm).collect();
134 let diff: f64 = x_new.iter().zip(x.iter()).map(|(a, b)| (a - b).abs()).sum();
135 x = x_new;
136 if diff < tol {
137 return Ok((lambda, x));
138 }
139 }
140 let lambda: f64 = {
141 let tx2 = self.tensor_vec_product(&x);
142 tx2.iter().zip(x.iter()).map(|(a, b)| a * b).sum()
143 };
144 Ok((lambda, x))
145 }
146}
147
148pub fn directed_motif_tensor(adj: &Array2<f64>) -> Array3<u8> {
160 let n = adj.nrows();
161 let mut tensor = Array3::<u8>::zeros((n, n, n));
162 for i in 0..n {
163 for j in 0..n {
164 if i == j || adj[[i, j]] == 0.0 {
165 continue;
166 }
167 for k in 0..n {
168 if k == i || k == j {
169 continue;
170 }
171 if adj[[j, k]] > 0.0 {
172 if adj[[k, i]] > 0.0 {
173 tensor[[i, j, k]] = 1;
175 } else {
176 tensor[[i, j, k]] = 2;
178 }
179 } else if adj[[i, k]] > 0.0 || adj[[k, j]] > 0.0 {
180 tensor[[i, j, k]] = 3;
181 }
182 }
183 }
184 }
185 tensor
186}
187
188#[derive(Debug, Clone)]
194pub struct TopologicalFeatures {
195 pub betti_numbers: Vec<usize>,
197 pub euler_characteristic: i64,
199 pub simplex_counts: Vec<usize>,
201 pub max_dim: usize,
203}
204
205impl TopologicalFeatures {
206 pub fn from_complex(sc: &SimplicialComplex) -> Self {
208 let max_dim = sc.max_dim().unwrap_or(0);
209 let betti_numbers = sc.betti_numbers();
210 let euler_characteristic = sc.euler_characteristic();
211 let simplex_counts = (0..=max_dim).map(|d| sc.num_simplices(d)).collect();
212 TopologicalFeatures {
213 betti_numbers,
214 euler_characteristic,
215 simplex_counts,
216 max_dim,
217 }
218 }
219
220 pub fn signature(&self, len: usize) -> Vec<f64> {
226 let half = len / 2;
227 let mut sig = Vec::with_capacity(len);
228
229 for i in 0..half {
231 sig.push(self.betti_numbers.get(i).copied().unwrap_or(0) as f64);
232 }
233 for i in 0..(len - half) {
235 sig.push(self.simplex_counts.get(i).copied().unwrap_or(0) as f64);
236 }
237
238 let norm: f64 = sig.iter().map(|x| x * x).sum::<f64>().sqrt();
240 if norm > 0.0 {
241 for v in &mut sig {
242 *v /= norm;
243 }
244 }
245 sig
246 }
247}
248
249#[derive(Debug, Clone)]
266pub struct CellularSheaf {
267 pub n_nodes: usize,
269 pub n_edges: usize,
271 pub node_stalks: Vec<usize>,
273 pub edge_stalks: Vec<usize>,
275 pub edges: Vec<(usize, usize)>,
277 pub restriction_maps: HashMap<(usize, usize), Vec<Vec<f64>>>,
280}
281
282impl CellularSheaf {
283 pub fn trivial(n_nodes: usize, edges: Vec<(usize, usize)>) -> Result<Self> {
292 for &(u, v) in &edges {
293 if u >= n_nodes || v >= n_nodes {
294 return Err(GraphError::InvalidGraph(format!(
295 "edge ({u},{v}) references node ≥ n_nodes={n_nodes}"
296 )));
297 }
298 }
299 let n_edges = edges.len();
300 let node_stalks = vec![1; n_nodes];
301 let edge_stalks = vec![1; n_edges];
302 let mut restriction_maps: HashMap<(usize, usize), Vec<Vec<f64>>> = HashMap::new();
303 for (eid, &(u, v)) in edges.iter().enumerate() {
304 restriction_maps.insert((u, eid), vec![vec![1.0]]);
307 restriction_maps.insert((v, eid), vec![vec![1.0]]);
308 }
309 Ok(CellularSheaf {
310 n_nodes,
311 n_edges,
312 node_stalks,
313 edge_stalks,
314 edges,
315 restriction_maps,
316 })
317 }
318
319 pub fn new(
325 n_nodes: usize,
326 node_dim: usize,
327 edges: Vec<(usize, usize)>,
328 edge_dim: usize,
329 maps: HashMap<(usize, usize), Vec<Vec<f64>>>,
330 ) -> Result<Self> {
331 let n_edges = edges.len();
332 for &(u, v) in &edges {
333 if u >= n_nodes || v >= n_nodes {
334 return Err(GraphError::InvalidGraph(format!(
335 "edge ({u},{v}) out of range"
336 )));
337 }
338 }
339 Ok(CellularSheaf {
340 n_nodes,
341 n_edges,
342 node_stalks: vec![node_dim; n_nodes],
343 edge_stalks: vec![edge_dim; n_edges],
344 edges,
345 restriction_maps: maps,
346 })
347 }
348
349 pub fn set_restriction(&mut self, v: usize, e: usize, map: Vec<Vec<f64>>) -> Result<()> {
353 if v >= self.n_nodes {
354 return Err(GraphError::InvalidGraph(format!(
355 "vertex {v} >= n_nodes {}",
356 self.n_nodes
357 )));
358 }
359 if e >= self.n_edges {
360 return Err(GraphError::InvalidGraph(format!(
361 "edge {e} >= n_edges {}",
362 self.n_edges
363 )));
364 }
365 self.restriction_maps.insert((v, e), map);
366 Ok(())
367 }
368
369 pub fn coboundary_operator(&self) -> Array2<f64> {
379 let total_node = self.node_stalks.iter().sum::<usize>();
380 let total_edge = self.edge_stalks.iter().sum::<usize>();
381
382 if total_node == 0 || total_edge == 0 {
383 return Array2::zeros((total_edge.max(1), total_node.max(1)));
384 }
385
386 let mut node_offset = vec![0usize; self.n_nodes + 1];
388 for i in 0..self.n_nodes {
389 node_offset[i + 1] = node_offset[i] + self.node_stalks[i];
390 }
391 let mut edge_offset = vec![0usize; self.n_edges + 1];
392 for i in 0..self.n_edges {
393 edge_offset[i + 1] = edge_offset[i] + self.edge_stalks[i];
394 }
395
396 let mut delta = Array2::<f64>::zeros((total_edge, total_node));
397
398 for (eid, &(u, v)) in self.edges.iter().enumerate() {
399 let e_rows = self.edge_stalks[eid];
400 let e_off = edge_offset[eid];
401
402 if let Some(fu) = self.restriction_maps.get(&(u, eid)) {
405 let u_off = node_offset[u];
406 let u_cols = self.node_stalks[u];
407 for r in 0..e_rows.min(fu.len()) {
409 for c in 0..u_cols.min(fu[r].len()) {
410 delta[[e_off + r, u_off + c]] -= fu[r][c];
411 }
412 }
413 }
414 if let Some(fv) = self.restriction_maps.get(&(v, eid)) {
416 let v_off = node_offset[v];
417 let v_cols = self.node_stalks[v];
418 for r in 0..e_rows.min(fv.len()) {
419 for c in 0..v_cols.min(fv[r].len()) {
420 delta[[e_off + r, v_off + c]] += fv[r][c];
421 }
422 }
423 }
424 }
425 delta
426 }
427
428 pub fn hodge_laplacian_0(&self) -> Array2<f64> {
432 let delta = self.coboundary_operator();
433 let rows = delta.nrows();
434 let cols = delta.ncols();
435 let mut l = Array2::<f64>::zeros((cols, cols));
437 for i in 0..cols {
438 for j in 0..cols {
439 let mut val = 0.0f64;
440 for r in 0..rows {
441 val += delta[[r, i]] * delta[[r, j]];
442 }
443 l[[i, j]] = val;
444 }
445 }
446 l
447 }
448
449 pub fn cohomology_h0(&self) -> usize {
456 let delta = self.coboundary_operator();
457 let total_node: usize = self.node_stalks.iter().sum();
458 let rank = matrix_rank_f64(&delta);
459 total_node.saturating_sub(rank)
460 }
461
462 pub fn cohomology_h1(&self) -> usize {
467 let delta = self.coboundary_operator();
468 let total_edge: usize = self.edge_stalks.iter().sum();
469 let rank = matrix_rank_f64(&delta);
470 total_edge.saturating_sub(rank)
471 }
472}
473
474pub fn trivial_sheaf_from_graph(n: usize, edge_list: &[(usize, usize)]) -> Result<CellularSheaf> {
478 let mut oriented: Vec<(usize, usize)> = edge_list
479 .iter()
480 .map(|&(u, v)| (u.min(v), u.max(v)))
481 .collect();
482 oriented.sort_unstable();
483 oriented.dedup();
484 CellularSheaf::trivial(n, oriented)
485}
486
487fn matrix_rank_f64(mat: &Array2<f64>) -> usize {
493 let (rows, cols) = (mat.nrows(), mat.ncols());
494 if rows == 0 || cols == 0 {
495 return 0;
496 }
497 let mut m: Vec<Vec<f64>> = (0..rows)
498 .map(|i| (0..cols).map(|j| mat[[i, j]]).collect())
499 .collect();
500
501 let tol = 1e-10;
502 let mut rank = 0usize;
503 let mut pivot_row = 0usize;
504 for col in 0..cols {
505 let (best, best_val) = (pivot_row..rows).fold((pivot_row, 0.0f64), |(bi, bv), r| {
507 let v = m[r][col].abs();
508 if v > bv {
509 (r, v)
510 } else {
511 (bi, bv)
512 }
513 });
514 if best_val < tol {
515 continue;
516 }
517 m.swap(pivot_row, best);
518 let piv = m[pivot_row][col];
519 for c in 0..cols {
520 m[pivot_row][c] /= piv;
521 }
522 for r in 0..rows {
523 if r == pivot_row {
524 continue;
525 }
526 let factor = m[r][col];
527 if factor.abs() < tol {
528 continue;
529 }
530 for c in 0..cols {
531 let sub = factor * m[pivot_row][c];
532 m[r][c] -= sub;
533 }
534 }
535 pivot_row += 1;
536 rank += 1;
537 }
538 rank
539}
540
541#[cfg(test)]
546mod tests {
547 use super::*;
548 use approx::assert_relative_eq;
549 use scirs2_core::ndarray::array;
550
551 #[test]
554 fn test_motif_tensor_triangle_count() {
555 let adj = array![[0.0_f64, 1., 1.], [1., 0., 1.], [1., 1., 0.]];
557 let mt = MotifTensor::from_adjacency(&adj);
558 assert_relative_eq!(mt.triangle_count(), 1.0, epsilon = 1e-10);
559 }
560
561 #[test]
562 fn test_motif_tensor_no_triangle() {
563 let adj = array![[0.0_f64, 1., 0.], [1., 0., 1.], [0., 1., 0.]];
565 let mt = MotifTensor::from_adjacency(&adj);
566 assert_relative_eq!(mt.triangle_count(), 0.0, epsilon = 1e-10);
567 }
568
569 #[test]
570 fn test_z_eigenvector_centrality_k3() {
571 let adj = array![[0.0_f64, 1., 1.], [1., 0., 1.], [1., 1., 0.]];
572 let mt = MotifTensor::from_adjacency(&adj);
573 let (lambda, x) = mt.z_eigenvector_centrality().expect("ok");
574 assert!(lambda >= 0.0);
575 for &xi in &x {
577 assert!(xi >= 0.0);
578 }
579 let norm: f64 = x.iter().map(|v| v * v).sum::<f64>().sqrt();
580 assert_relative_eq!(norm, 1.0, epsilon = 1e-6);
581 }
582
583 #[test]
584 fn test_directed_motif_tensor_cycle() {
585 let mut adj = Array2::<f64>::zeros((3, 3));
587 adj[[0, 1]] = 1.0;
588 adj[[1, 2]] = 1.0;
589 adj[[2, 0]] = 1.0;
590 let mt = directed_motif_tensor(&adj);
591 assert_eq!(mt[[0, 1, 2]], 1); }
593
594 #[test]
597 fn test_topological_features_triangle() {
598 let mut sc = SimplicialComplex::new();
599 sc.add_simplex(vec![0, 1, 2]);
600 let tf = TopologicalFeatures::from_complex(&sc);
601 assert_eq!(tf.betti_numbers[0], 1);
602 assert_eq!(tf.euler_characteristic, 1); }
604
605 #[test]
606 fn test_signature_length() {
607 let mut sc = SimplicialComplex::new();
608 sc.add_simplex(vec![0, 1]);
609 let tf = TopologicalFeatures::from_complex(&sc);
610 let sig = tf.signature(8);
611 assert_eq!(sig.len(), 8);
612 }
613
614 #[test]
617 fn test_trivial_sheaf_path() {
618 let sheaf = CellularSheaf::trivial(3, vec![(0, 1), (1, 2)]).expect("ok");
620 let delta = sheaf.coboundary_operator();
621 assert_eq!(delta.nrows(), 2);
623 assert_eq!(delta.ncols(), 3);
624 }
625
626 #[test]
627 fn test_trivial_sheaf_h0_path() {
628 let sheaf = CellularSheaf::trivial(3, vec![(0, 1), (1, 2)]).expect("ok");
630 assert_eq!(sheaf.cohomology_h0(), 1);
631 }
632
633 #[test]
634 fn test_trivial_sheaf_h0_two_components() {
635 let sheaf = CellularSheaf::trivial(4, vec![(0, 1), (2, 3)]).expect("ok");
637 assert_eq!(sheaf.cohomology_h0(), 2);
638 }
639
640 #[test]
641 fn test_trivial_sheaf_h1_triangle() {
642 let sheaf = CellularSheaf::trivial(3, vec![(0, 1), (0, 2), (1, 2)]).expect("ok");
644 assert_eq!(sheaf.cohomology_h1(), 1);
645 }
646
647 #[test]
648 fn test_hodge_laplacian_symmetric() {
649 let sheaf = CellularSheaf::trivial(3, vec![(0, 1), (1, 2)]).expect("ok");
650 let l = sheaf.hodge_laplacian_0();
651 for i in 0..l.nrows() {
653 for j in 0..l.ncols() {
654 assert_relative_eq!(l[[i, j]], l[[j, i]], epsilon = 1e-10);
655 }
656 }
657 }
658
659 #[test]
660 fn test_trivial_sheaf_from_graph() {
661 let sheaf = trivial_sheaf_from_graph(4, &[(0, 1), (1, 2), (2, 3)]).expect("ok");
662 assert_eq!(sheaf.n_nodes, 4);
663 assert_eq!(sheaf.n_edges, 3);
664 }
665
666 #[test]
667 fn test_sheaf_invalid_node() {
668 let err = CellularSheaf::trivial(3, vec![(0, 5)]);
669 assert!(err.is_err());
670 }
671}