1use crate::base::Hypergraph as BaseHypergraph;
13use crate::base::{EdgeWeight, Graph, IndexType, Node};
14use crate::error::{GraphError, Result};
15use scirs2_core::ndarray::Array2;
16use scirs2_core::random::{Rng, RngExt, SeedableRng};
17use std::collections::{HashMap, HashSet};
18
19#[derive(Debug, Clone, PartialEq)]
28pub struct Hyperedge {
29 pub id: usize,
31 pub nodes: Vec<usize>,
33 pub weight: f64,
35}
36
37impl Hyperedge {
38 pub fn new(id: usize, mut nodes: Vec<usize>, weight: f64) -> Self {
40 nodes.sort_unstable();
41 nodes.dedup();
42 Hyperedge { id, nodes, weight }
43 }
44
45 pub fn size(&self) -> usize {
47 self.nodes.len()
48 }
49
50 pub fn contains(&self, node: usize) -> bool {
52 self.nodes.binary_search(&node).is_ok()
53 }
54
55 pub fn intersection_size(&self, other: &Hyperedge) -> usize {
57 let mut i = 0;
58 let mut j = 0;
59 let mut count = 0;
60 while i < self.nodes.len() && j < other.nodes.len() {
61 match self.nodes[i].cmp(&other.nodes[j]) {
62 std::cmp::Ordering::Less => i += 1,
63 std::cmp::Ordering::Greater => j += 1,
64 std::cmp::Ordering::Equal => {
65 count += 1;
66 i += 1;
67 j += 1;
68 }
69 }
70 }
71 count
72 }
73}
74
75#[derive(Debug, Clone)]
93pub struct IndexedHypergraph {
94 n_nodes: usize,
96 hyperedges: Vec<Hyperedge>,
98 node_to_hyperedges: Vec<Vec<usize>>,
100}
101
102impl IndexedHypergraph {
103 pub fn new(n_nodes: usize) -> Self {
105 IndexedHypergraph {
106 n_nodes,
107 hyperedges: Vec::new(),
108 node_to_hyperedges: vec![Vec::new(); n_nodes],
109 }
110 }
111
112 pub fn n_nodes(&self) -> usize {
114 self.n_nodes
115 }
116
117 pub fn n_hyperedges(&self) -> usize {
119 self.hyperedges.len()
120 }
121
122 pub fn hyperedges(&self) -> &[Hyperedge] {
124 &self.hyperedges
125 }
126
127 pub fn add_hyperedge(&mut self, nodes: Vec<usize>, weight: f64) -> Result<usize> {
131 if weight < 0.0 {
132 return Err(GraphError::InvalidGraph(
133 "hyperedge weight must be non-negative".to_string(),
134 ));
135 }
136 for &n in &nodes {
137 if n >= self.n_nodes {
138 return Err(GraphError::InvalidGraph(format!(
139 "node {n} out of range (n_nodes = {})",
140 self.n_nodes
141 )));
142 }
143 }
144 let id = self.hyperedges.len();
145 let he = Hyperedge::new(id, nodes, weight);
146 for &n in &he.nodes {
147 self.node_to_hyperedges[n].push(id);
148 }
149 self.hyperedges.push(he);
150 Ok(id)
151 }
152
153 pub fn hyperedges_of_node(&self, node: usize) -> Option<&[usize]> {
155 if node < self.n_nodes {
156 Some(&self.node_to_hyperedges[node])
157 } else {
158 None
159 }
160 }
161
162 pub fn degree(&self, node: usize) -> usize {
164 self.node_to_hyperedges
165 .get(node)
166 .map(|v| v.len())
167 .unwrap_or(0)
168 }
169
170 pub fn weighted_degree(&self, node: usize) -> f64 {
172 self.node_to_hyperedges
173 .get(node)
174 .map(|ids| ids.iter().map(|&id| self.hyperedges[id].weight).sum())
175 .unwrap_or(0.0)
176 }
177
178 pub fn incidence_matrix(&self) -> Array2<f64> {
182 let m = self.n_nodes;
183 let e = self.hyperedges.len();
184 let mut b = Array2::<f64>::zeros((m, e));
185 for (eid, he) in self.hyperedges.iter().enumerate() {
186 let sw = he.weight.sqrt();
187 for &n in &he.nodes {
188 b[[n, eid]] = sw;
189 }
190 }
191 b
192 }
193
194 pub fn incidence_matrix_binary(&self) -> Array2<f64> {
196 let m = self.n_nodes;
197 let e = self.hyperedges.len();
198 let mut b = Array2::<f64>::zeros((m, e));
199 for (eid, he) in self.hyperedges.iter().enumerate() {
200 for &n in &he.nodes {
201 b[[n, eid]] = 1.0;
202 }
203 }
204 b
205 }
206
207 pub fn star_expansion(&self) -> Graph<usize, f64> {
214 let total = self.n_nodes + self.hyperedges.len();
215 let mut g: Graph<usize, f64> = Graph::new();
216 for i in 0..total {
217 g.add_node(i);
218 }
219 for (eid, he) in self.hyperedges.iter().enumerate() {
220 let aux = self.n_nodes + eid;
221 for &n in &he.nodes {
222 let _ = g.add_edge(n, aux, he.weight);
223 }
224 }
225 g
226 }
227
228 pub fn bipartite_representation(&self) -> BaseHypergraph<usize, f64> {
236 let mut bg: BaseHypergraph<usize, f64> = BaseHypergraph::new();
237 for i in 0..self.n_nodes {
239 bg.add_node(i);
240 }
241 for (eid, he) in self.hyperedges.iter().enumerate() {
243 let he_node = self.n_nodes + eid;
244 bg.add_node(he_node);
245 for &n in &he.nodes {
246 let _ = bg.add_hyperedge_from_vec(vec![n, he_node], he.weight);
247 }
248 }
249 bg
250 }
251}
252
253#[derive(Debug, Clone)]
276pub struct Hypergraph<N, E> {
277 nodes: Vec<N>,
279 hyperedges: Vec<(E, Vec<usize>)>,
281 node_to_edges: Vec<Vec<usize>>,
283}
284
285impl<N: Clone, E: Clone> Default for Hypergraph<N, E> {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291impl<N: Clone, E: Clone> Hypergraph<N, E> {
292 pub fn new() -> Self {
294 Hypergraph {
295 nodes: Vec::new(),
296 hyperedges: Vec::new(),
297 node_to_edges: Vec::new(),
298 }
299 }
300
301 pub fn add_node(&mut self, data: N) -> usize {
303 let id = self.nodes.len();
304 self.nodes.push(data);
305 self.node_to_edges.push(Vec::new());
306 id
307 }
308
309 pub fn add_hyperedge(&mut self, data: E, mut nodes: Vec<usize>) -> usize {
315 nodes.sort_unstable();
316 nodes.dedup();
317 let id = self.hyperedges.len();
318 for &n in &nodes {
319 if n < self.node_to_edges.len() {
320 self.node_to_edges[n].push(id);
321 }
322 }
323 self.hyperedges.push((data, nodes));
324 id
325 }
326
327 pub fn try_add_hyperedge(&mut self, data: E, mut nodes: Vec<usize>) -> Result<usize> {
330 nodes.sort_unstable();
331 nodes.dedup();
332 for &n in &nodes {
333 if n >= self.nodes.len() {
334 return Err(GraphError::InvalidGraph(format!(
335 "node index {n} out of range (n_nodes = {})",
336 self.nodes.len()
337 )));
338 }
339 }
340 let id = self.hyperedges.len();
341 for &n in &nodes {
342 self.node_to_edges[n].push(id);
343 }
344 self.hyperedges.push((data, nodes));
345 Ok(id)
346 }
347
348 pub fn incident_edges(&self, node: usize) -> Vec<usize> {
350 self.node_to_edges.get(node).cloned().unwrap_or_default()
351 }
352
353 pub fn node_degree(&self, node: usize) -> usize {
355 self.node_to_edges.get(node).map(|v| v.len()).unwrap_or(0)
356 }
357
358 pub fn node_count(&self) -> usize {
360 self.nodes.len()
361 }
362
363 pub fn hyperedge_count(&self) -> usize {
365 self.hyperedges.len()
366 }
367
368 pub fn node_data(&self, id: usize) -> Option<&N> {
370 self.nodes.get(id)
371 }
372
373 pub fn hyperedge_data(&self, id: usize) -> Option<(&E, &[usize])> {
375 self.hyperedges.get(id).map(|(e, ns)| (e, ns.as_slice()))
376 }
377
378 pub fn hyperedge_nodes(&self, id: usize) -> Option<&[usize]> {
380 self.hyperedges.get(id).map(|(_, ns)| ns.as_slice())
381 }
382}
383
384impl<N: Clone + std::fmt::Debug + std::hash::Hash + Eq + Ord, E: Clone> Hypergraph<N, E> {
385 pub fn clique_expansion_indexed(&self) -> Graph<usize, f64> {
391 let mut g: Graph<usize, f64> = Graph::new();
392 for i in 0..self.nodes.len() {
393 g.add_node(i);
394 }
395 let mut weights: HashMap<(usize, usize), f64> = HashMap::new();
396 for (_, nodes) in &self.hyperedges {
397 let k = nodes.len();
398 if k < 2 {
399 continue;
400 }
401 let contrib = 1.0 / (k - 1) as f64;
402 for i in 0..k {
403 for j in (i + 1)..k {
404 *weights.entry((nodes[i], nodes[j])).or_insert(0.0) += contrib;
405 }
406 }
407 }
408 for ((u, v), w) in weights {
409 let _ = g.add_edge(u, v, w);
410 }
411 g
412 }
413}
414
415pub fn clique_expansion(hg: &IndexedHypergraph) -> Graph<usize, f64> {
425 let mut graph: Graph<usize, f64> = Graph::new();
426 for i in 0..hg.n_nodes() {
427 graph.add_node(i);
428 }
429 let mut edge_weights: HashMap<(usize, usize), f64> = HashMap::new();
430 for he in &hg.hyperedges {
431 let k = he.nodes.len();
432 if k < 2 {
433 continue;
434 }
435 let contrib = he.weight / (k - 1) as f64;
436 for i in 0..k {
437 for j in (i + 1)..k {
438 let key = (he.nodes[i], he.nodes[j]);
439 *edge_weights.entry(key).or_insert(0.0) += contrib;
440 }
441 }
442 }
443 for ((u, v), w) in edge_weights {
444 let _ = graph.add_edge(u, v, w);
445 }
446 graph
447}
448
449pub fn line_graph(hg: &IndexedHypergraph) -> Graph<usize, f64> {
459 let e = hg.n_hyperedges();
460 let mut graph: Graph<usize, f64> = Graph::new();
461 for i in 0..e {
462 graph.add_node(i);
463 }
464 for i in 0..e {
465 for j in (i + 1)..e {
466 let shared = hg.hyperedges[i].intersection_size(&hg.hyperedges[j]);
467 if shared > 0 {
468 let _ = graph.add_edge(i, j, shared as f64);
469 }
470 }
471 }
472 graph
473}
474
475pub fn hypergraph_random_walk<R: Rng>(
487 hg: &IndexedHypergraph,
488 start: usize,
489 n_steps: usize,
490 rng: &mut R,
491) -> Result<Vec<usize>> {
492 if hg.n_nodes() == 0 {
493 return Err(GraphError::InvalidGraph(
494 "hypergraph has no nodes".to_string(),
495 ));
496 }
497 if start >= hg.n_nodes() {
498 return Err(GraphError::InvalidGraph(format!(
499 "start node {start} >= n_nodes {}",
500 hg.n_nodes()
501 )));
502 }
503 let mut path = Vec::with_capacity(n_steps + 1);
504 let mut current = start;
505 path.push(current);
506
507 for _ in 0..n_steps {
508 let ids = match hg.hyperedges_of_node(current) {
509 Some(ids) if !ids.is_empty() => ids,
510 _ => {
511 path.push(current);
512 continue;
513 }
514 };
515 let total_w: f64 = ids.iter().map(|&id| hg.hyperedges[id].weight).sum();
516 let threshold = rng.random::<f64>() * total_w;
517 let mut accum = 0.0;
518 let mut chosen_id = ids[ids.len() - 1];
520 for &id in ids {
521 accum += hg.hyperedges[id].weight;
522 if accum >= threshold {
523 chosen_id = id;
524 break;
525 }
526 }
527 let he = &hg.hyperedges[chosen_id];
528 let candidates: Vec<usize> = he.nodes.iter().copied().filter(|&n| n != current).collect();
529 if candidates.is_empty() {
530 path.push(current);
531 } else {
532 let idx = rng.random_range(0..candidates.len());
533 current = candidates[idx];
534 path.push(current);
535 }
536 }
537 Ok(path)
538}
539
540pub fn hypergraph_random_walk_seeded(
542 hg: &IndexedHypergraph,
543 start: usize,
544 n_steps: usize,
545 seed: u64,
546) -> Result<Vec<usize>> {
547 use scirs2_core::random::ChaCha20Rng;
548 let mut rng = ChaCha20Rng::seed_from_u64(seed);
549 hypergraph_random_walk(hg, start, n_steps, &mut rng)
550}
551
552pub fn hyperedge_centrality(hg: &IndexedHypergraph) -> Vec<f64> {
560 let m = hg.n_nodes();
561 let e = hg.n_hyperedges();
562 if e == 0 || m == 0 {
563 return Vec::new();
564 }
565 let dv: Vec<f64> = (0..m).map(|i| hg.weighted_degree(i)).collect();
566 let de: Vec<f64> = hg.hyperedges.iter().map(|h| h.nodes.len() as f64).collect();
567
568 let mut c = vec![1.0 / e as f64; e];
569 let max_iter = 1000;
570 let tol = 1e-9;
571
572 for _ in 0..max_iter {
573 let x: Vec<f64> = c
574 .iter()
575 .enumerate()
576 .map(|(eid, &cv)| if de[eid] > 0.0 { cv / de[eid] } else { 0.0 })
577 .collect();
578 let mut y = vec![0.0f64; m];
579 for (eid, he) in hg.hyperedges.iter().enumerate() {
580 let sw = he.weight.sqrt();
581 for &n in &he.nodes {
582 y[n] += sw * x[eid];
583 }
584 }
585 let z: Vec<f64> = y
586 .iter()
587 .enumerate()
588 .map(|(i, &yi)| if dv[i] > 0.0 { yi / dv[i] } else { 0.0 })
589 .collect();
590 let mut c_new = vec![0.0f64; e];
591 for (eid, he) in hg.hyperedges.iter().enumerate() {
592 let sw = he.weight.sqrt();
593 for &n in &he.nodes {
594 c_new[eid] += sw * z[n];
595 }
596 }
597 let norm: f64 = c_new.iter().map(|v| v * v).sum::<f64>().sqrt();
598 if norm == 0.0 {
599 return vec![0.0; e];
600 }
601 for v in &mut c_new {
602 *v /= norm;
603 }
604 let diff: f64 = c_new.iter().zip(c.iter()).map(|(a, b)| (a - b).abs()).sum();
605 c = c_new;
606 if diff < tol {
607 break;
608 }
609 }
610
611 let total: f64 = c.iter().map(|v| v.abs()).sum();
612 if total > 0.0 {
613 c.iter().map(|v| v.abs() / total).collect()
614 } else {
615 vec![0.0; e]
616 }
617}
618
619pub fn hypergraph_clustering_coefficient(node: usize, hg: &IndexedHypergraph) -> f64 {
628 if node >= hg.n_nodes() {
629 return 0.0;
630 }
631 let mut neighbour_set: HashSet<usize> = HashSet::new();
632 if let Some(ids) = hg.hyperedges_of_node(node) {
633 for &eid in ids {
634 for &n in &hg.hyperedges[eid].nodes {
635 if n != node {
636 neighbour_set.insert(n);
637 }
638 }
639 }
640 }
641 let neighbours: Vec<usize> = neighbour_set.into_iter().collect();
642 let k = neighbours.len();
643 if k < 2 {
644 return 0.0;
645 }
646 let mut connected_pairs: usize = 0;
647 let total_pairs = k * (k - 1) / 2;
648 for i in 0..k {
649 for j in (i + 1)..k {
650 let u = neighbours[i];
651 let v = neighbours[j];
652 let u_edges: HashSet<usize> = hg
653 .hyperedges_of_node(u)
654 .unwrap_or(&[])
655 .iter()
656 .copied()
657 .collect();
658 let v_edges = hg.hyperedges_of_node(v).unwrap_or(&[]);
659 if v_edges.iter().any(|id| u_edges.contains(id)) {
660 connected_pairs += 1;
661 }
662 }
663 }
664 connected_pairs as f64 / total_pairs as f64
665}
666
667#[cfg(test)]
672mod tests {
673 use super::*;
674 use approx::assert_relative_eq;
675
676 fn triangle_hg() -> IndexedHypergraph {
677 let mut hg = IndexedHypergraph::new(3);
678 hg.add_hyperedge(vec![0, 1], 1.0).expect("add ok");
679 hg.add_hyperedge(vec![1, 2], 1.0).expect("add ok");
680 hg.add_hyperedge(vec![0, 2], 1.0).expect("add ok");
681 hg
682 }
683
684 #[test]
685 fn test_hyperedge_dedup() {
686 let he = Hyperedge::new(0, vec![3, 1, 2, 1], 1.0);
687 assert_eq!(he.nodes, vec![1, 2, 3]);
688 }
689
690 #[test]
691 fn test_hyperedge_contains() {
692 let he = Hyperedge::new(0, vec![0, 2, 4], 1.0);
693 assert!(he.contains(0));
694 assert!(!he.contains(1));
695 }
696
697 #[test]
698 fn test_intersection_size() {
699 let a = Hyperedge::new(0, vec![0, 1, 2], 1.0);
700 let b = Hyperedge::new(1, vec![1, 2, 3], 1.0);
701 assert_eq!(a.intersection_size(&b), 2);
702 }
703
704 #[test]
705 fn test_add_hyperedge_invalid_node() {
706 let mut hg = IndexedHypergraph::new(3);
707 assert!(hg.add_hyperedge(vec![0, 5], 1.0).is_err());
708 }
709
710 #[test]
711 fn test_add_hyperedge_negative_weight() {
712 let mut hg = IndexedHypergraph::new(3);
713 assert!(hg.add_hyperedge(vec![0, 1], -1.0).is_err());
714 }
715
716 #[test]
717 fn test_degree_and_weighted_degree() {
718 let mut hg = IndexedHypergraph::new(3);
719 hg.add_hyperedge(vec![0, 1], 2.0).expect("ok");
720 hg.add_hyperedge(vec![0, 2], 3.0).expect("ok");
721 assert_eq!(hg.degree(0), 2);
722 assert_relative_eq!(hg.weighted_degree(0), 5.0, epsilon = 1e-10);
723 }
724
725 #[test]
726 fn test_incidence_matrix_shape() {
727 let hg = triangle_hg();
728 assert_eq!(hg.incidence_matrix().shape(), &[3, 3]);
729 }
730
731 #[test]
732 fn test_clique_expansion_triangle() {
733 let hg = triangle_hg();
734 let g = clique_expansion(&hg);
735 assert_eq!(g.node_count(), 3);
736 assert_eq!(g.edge_count(), 3);
737 }
738
739 #[test]
740 fn test_star_expansion_node_count() {
741 let hg = triangle_hg(); let g = hg.star_expansion();
743 assert_eq!(g.node_count(), 6);
744 assert_eq!(g.edge_count(), 6);
746 }
747
748 #[test]
749 fn test_bipartite_representation() {
750 let mut hg = IndexedHypergraph::new(3);
751 hg.add_hyperedge(vec![0, 1], 1.0).expect("ok");
752 let bg = hg.bipartite_representation();
753 assert_eq!(bg.node_count(), 4);
755 }
756
757 #[test]
758 fn test_generic_hypergraph_degree() {
759 let mut hg: Hypergraph<&str, &str> = Hypergraph::new();
760 let a = hg.add_node("a");
761 let b = hg.add_node("b");
762 let c = hg.add_node("c");
763 hg.add_hyperedge("e1", vec![a, b, c]);
764 hg.add_hyperedge("e2", vec![a, b]);
765 assert_eq!(hg.node_degree(a), 2);
766 assert_eq!(hg.node_degree(c), 1);
767 }
768
769 #[test]
770 fn test_generic_try_add_hyperedge_oob() {
771 let mut hg: Hypergraph<i32, i32> = Hypergraph::new();
772 hg.add_node(0);
773 assert!(hg.try_add_hyperedge(99, vec![0, 5]).is_err());
774 }
775
776 #[test]
777 fn test_line_graph_triangle() {
778 let hg = triangle_hg();
779 let lg = line_graph(&hg);
780 assert_eq!(lg.node_count(), 3);
781 assert_eq!(lg.edge_count(), 3);
782 }
783
784 #[test]
785 fn test_random_walk_length() {
786 let hg = triangle_hg();
787 let path = hypergraph_random_walk_seeded(&hg, 0, 10, 99).expect("ok");
788 assert_eq!(path.len(), 11);
789 }
790
791 #[test]
792 fn test_hyperedge_centrality_sums_to_one() {
793 let hg = triangle_hg();
794 let c = hyperedge_centrality(&hg);
795 let sum: f64 = c.iter().sum();
796 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
797 }
798
799 #[test]
800 fn test_clustering_coeff_full_triangle() {
801 let hg = triangle_hg();
802 assert_relative_eq!(
803 hypergraph_clustering_coefficient(0, &hg),
804 1.0,
805 epsilon = 1e-10
806 );
807 }
808}