use crate::base::{Graph, IndexType, Node, EdgeWeight};
use crate::base::Hypergraph as BaseHypergraph;
use crate::error::{GraphError, Result};
use scirs2_core::ndarray::Array2;
use scirs2_core::random::{Rng, SeedableRng};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)]
pub struct Hyperedge {
pub id: usize,
pub nodes: Vec<usize>,
pub weight: f64,
}
impl Hyperedge {
pub fn new(id: usize, mut nodes: Vec<usize>, weight: f64) -> Self {
nodes.sort_unstable();
nodes.dedup();
Hyperedge { id, nodes, weight }
}
pub fn size(&self) -> usize {
self.nodes.len()
}
pub fn contains(&self, node: usize) -> bool {
self.nodes.binary_search(&node).is_ok()
}
pub fn intersection_size(&self, other: &Hyperedge) -> usize {
let mut i = 0;
let mut j = 0;
let mut count = 0;
while i < self.nodes.len() && j < other.nodes.len() {
match self.nodes[i].cmp(&other.nodes[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
count += 1;
i += 1;
j += 1;
}
}
}
count
}
}
#[derive(Debug, Clone)]
pub struct IndexedHypergraph {
n_nodes: usize,
hyperedges: Vec<Hyperedge>,
node_to_hyperedges: Vec<Vec<usize>>,
}
impl IndexedHypergraph {
pub fn new(n_nodes: usize) -> Self {
IndexedHypergraph {
n_nodes,
hyperedges: Vec::new(),
node_to_hyperedges: vec![Vec::new(); n_nodes],
}
}
pub fn n_nodes(&self) -> usize {
self.n_nodes
}
pub fn n_hyperedges(&self) -> usize {
self.hyperedges.len()
}
pub fn hyperedges(&self) -> &[Hyperedge] {
&self.hyperedges
}
pub fn add_hyperedge(&mut self, nodes: Vec<usize>, weight: f64) -> Result<usize> {
if weight < 0.0 {
return Err(GraphError::InvalidGraph(
"hyperedge weight must be non-negative".to_string(),
));
}
for &n in &nodes {
if n >= self.n_nodes {
return Err(GraphError::InvalidGraph(format!(
"node {n} out of range (n_nodes = {})",
self.n_nodes
)));
}
}
let id = self.hyperedges.len();
let he = Hyperedge::new(id, nodes, weight);
for &n in &he.nodes {
self.node_to_hyperedges[n].push(id);
}
self.hyperedges.push(he);
Ok(id)
}
pub fn hyperedges_of_node(&self, node: usize) -> Option<&[usize]> {
if node < self.n_nodes {
Some(&self.node_to_hyperedges[node])
} else {
None
}
}
pub fn degree(&self, node: usize) -> usize {
self.node_to_hyperedges
.get(node)
.map(|v| v.len())
.unwrap_or(0)
}
pub fn weighted_degree(&self, node: usize) -> f64 {
self.node_to_hyperedges
.get(node)
.map(|ids| ids.iter().map(|&id| self.hyperedges[id].weight).sum())
.unwrap_or(0.0)
}
pub fn incidence_matrix(&self) -> Array2<f64> {
let m = self.n_nodes;
let e = self.hyperedges.len();
let mut b = Array2::<f64>::zeros((m, e));
for (eid, he) in self.hyperedges.iter().enumerate() {
let sw = he.weight.sqrt();
for &n in &he.nodes {
b[[n, eid]] = sw;
}
}
b
}
pub fn incidence_matrix_binary(&self) -> Array2<f64> {
let m = self.n_nodes;
let e = self.hyperedges.len();
let mut b = Array2::<f64>::zeros((m, e));
for (eid, he) in self.hyperedges.iter().enumerate() {
for &n in &he.nodes {
b[[n, eid]] = 1.0;
}
}
b
}
pub fn star_expansion(&self) -> Graph<usize, f64> {
let total = self.n_nodes + self.hyperedges.len();
let mut g: Graph<usize, f64> = Graph::new();
for i in 0..total {
g.add_node(i);
}
for (eid, he) in self.hyperedges.iter().enumerate() {
let aux = self.n_nodes + eid;
for &n in &he.nodes {
let _ = g.add_edge(n, aux, he.weight);
}
}
g
}
pub fn bipartite_representation(&self) -> BaseHypergraph<usize, f64> {
let mut bg: BaseHypergraph<usize, f64> = BaseHypergraph::new();
for i in 0..self.n_nodes {
bg.add_node(i);
}
for (eid, he) in self.hyperedges.iter().enumerate() {
let he_node = self.n_nodes + eid;
bg.add_node(he_node);
for &n in &he.nodes {
let _ = bg.add_hyperedge_from_vec(vec![n, he_node], he.weight);
}
}
bg
}
}
#[derive(Debug, Clone)]
pub struct Hypergraph<N, E> {
nodes: Vec<N>,
hyperedges: Vec<(E, Vec<usize>)>,
node_to_edges: Vec<Vec<usize>>,
}
impl<N: Clone, E: Clone> Default for Hypergraph<N, E> {
fn default() -> Self {
Self::new()
}
}
impl<N: Clone, E: Clone> Hypergraph<N, E> {
pub fn new() -> Self {
Hypergraph {
nodes: Vec::new(),
hyperedges: Vec::new(),
node_to_edges: Vec::new(),
}
}
pub fn add_node(&mut self, data: N) -> usize {
let id = self.nodes.len();
self.nodes.push(data);
self.node_to_edges.push(Vec::new());
id
}
pub fn add_hyperedge(&mut self, data: E, mut nodes: Vec<usize>) -> usize {
nodes.sort_unstable();
nodes.dedup();
let id = self.hyperedges.len();
for &n in &nodes {
if n < self.node_to_edges.len() {
self.node_to_edges[n].push(id);
}
}
self.hyperedges.push((data, nodes));
id
}
pub fn try_add_hyperedge(&mut self, data: E, mut nodes: Vec<usize>) -> Result<usize> {
nodes.sort_unstable();
nodes.dedup();
for &n in &nodes {
if n >= self.nodes.len() {
return Err(GraphError::InvalidGraph(format!(
"node index {n} out of range (n_nodes = {})",
self.nodes.len()
)));
}
}
let id = self.hyperedges.len();
for &n in &nodes {
self.node_to_edges[n].push(id);
}
self.hyperedges.push((data, nodes));
Ok(id)
}
pub fn incident_edges(&self, node: usize) -> Vec<usize> {
self.node_to_edges
.get(node)
.cloned()
.unwrap_or_default()
}
pub fn node_degree(&self, node: usize) -> usize {
self.node_to_edges
.get(node)
.map(|v| v.len())
.unwrap_or(0)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn hyperedge_count(&self) -> usize {
self.hyperedges.len()
}
pub fn node_data(&self, id: usize) -> Option<&N> {
self.nodes.get(id)
}
pub fn hyperedge_data(&self, id: usize) -> Option<(&E, &[usize])> {
self.hyperedges.get(id).map(|(e, ns)| (e, ns.as_slice()))
}
pub fn hyperedge_nodes(&self, id: usize) -> Option<&[usize]> {
self.hyperedges.get(id).map(|(_, ns)| ns.as_slice())
}
}
impl<N: Clone + std::fmt::Debug + std::hash::Hash + Eq + Ord, E: Clone> Hypergraph<N, E> {
pub fn clique_expansion_indexed(&self) -> Graph<usize, f64> {
let mut g: Graph<usize, f64> = Graph::new();
for i in 0..self.nodes.len() {
g.add_node(i);
}
let mut weights: HashMap<(usize, usize), f64> = HashMap::new();
for (_, nodes) in &self.hyperedges {
let k = nodes.len();
if k < 2 {
continue;
}
let contrib = 1.0 / (k - 1) as f64;
for i in 0..k {
for j in (i + 1)..k {
*weights.entry((nodes[i], nodes[j])).or_insert(0.0) += contrib;
}
}
}
for ((u, v), w) in weights {
let _ = g.add_edge(u, v, w);
}
g
}
}
pub fn clique_expansion(hg: &IndexedHypergraph) -> Graph<usize, f64> {
let mut graph: Graph<usize, f64> = Graph::new();
for i in 0..hg.n_nodes() {
graph.add_node(i);
}
let mut edge_weights: HashMap<(usize, usize), f64> = HashMap::new();
for he in &hg.hyperedges {
let k = he.nodes.len();
if k < 2 {
continue;
}
let contrib = he.weight / (k - 1) as f64;
for i in 0..k {
for j in (i + 1)..k {
let key = (he.nodes[i], he.nodes[j]);
*edge_weights.entry(key).or_insert(0.0) += contrib;
}
}
}
for ((u, v), w) in edge_weights {
let _ = graph.add_edge(u, v, w);
}
graph
}
pub fn line_graph(hg: &IndexedHypergraph) -> Graph<usize, f64> {
let e = hg.n_hyperedges();
let mut graph: Graph<usize, f64> = Graph::new();
for i in 0..e {
graph.add_node(i);
}
for i in 0..e {
for j in (i + 1)..e {
let shared = hg.hyperedges[i].intersection_size(&hg.hyperedges[j]);
if shared > 0 {
let _ = graph.add_edge(i, j, shared as f64);
}
}
}
graph
}
pub fn hypergraph_random_walk<R: Rng>(
hg: &IndexedHypergraph,
start: usize,
n_steps: usize,
rng: &mut R,
) -> Result<Vec<usize>> {
if hg.n_nodes() == 0 {
return Err(GraphError::InvalidGraph(
"hypergraph has no nodes".to_string(),
));
}
if start >= hg.n_nodes() {
return Err(GraphError::InvalidGraph(format!(
"start node {start} >= n_nodes {}",
hg.n_nodes()
)));
}
let mut path = Vec::with_capacity(n_steps + 1);
let mut current = start;
path.push(current);
for _ in 0..n_steps {
let ids = match hg.hyperedges_of_node(current) {
Some(ids) if !ids.is_empty() => ids,
_ => {
path.push(current);
continue;
}
};
let total_w: f64 = ids.iter().map(|&id| hg.hyperedges[id].weight).sum();
let threshold = rng.random::<f64>() * total_w;
let mut accum = 0.0;
let mut chosen_id = ids[ids.len() - 1];
for &id in ids {
accum += hg.hyperedges[id].weight;
if accum >= threshold {
chosen_id = id;
break;
}
}
let he = &hg.hyperedges[chosen_id];
let candidates: Vec<usize> = he
.nodes
.iter()
.copied()
.filter(|&n| n != current)
.collect();
if candidates.is_empty() {
path.push(current);
} else {
let idx = rng.random_range(0..candidates.len());
current = candidates[idx];
path.push(current);
}
}
Ok(path)
}
pub fn hypergraph_random_walk_seeded(
hg: &IndexedHypergraph,
start: usize,
n_steps: usize,
seed: u64,
) -> Result<Vec<usize>> {
use scirs2_core::random::ChaCha20Rng;
let mut rng = ChaCha20Rng::seed_from_u64(seed);
hypergraph_random_walk(hg, start, n_steps, &mut rng)
}
pub fn hyperedge_centrality(hg: &IndexedHypergraph) -> Vec<f64> {
let m = hg.n_nodes();
let e = hg.n_hyperedges();
if e == 0 || m == 0 {
return Vec::new();
}
let dv: Vec<f64> = (0..m).map(|i| hg.weighted_degree(i)).collect();
let de: Vec<f64> = hg
.hyperedges
.iter()
.map(|h| h.nodes.len() as f64)
.collect();
let mut c = vec![1.0 / e as f64; e];
let max_iter = 1000;
let tol = 1e-9;
for _ in 0..max_iter {
let x: Vec<f64> = c
.iter()
.enumerate()
.map(|(eid, &cv)| if de[eid] > 0.0 { cv / de[eid] } else { 0.0 })
.collect();
let mut y = vec![0.0f64; m];
for (eid, he) in hg.hyperedges.iter().enumerate() {
let sw = he.weight.sqrt();
for &n in &he.nodes {
y[n] += sw * x[eid];
}
}
let z: Vec<f64> = y
.iter()
.enumerate()
.map(|(i, &yi)| if dv[i] > 0.0 { yi / dv[i] } else { 0.0 })
.collect();
let mut c_new = vec![0.0f64; e];
for (eid, he) in hg.hyperedges.iter().enumerate() {
let sw = he.weight.sqrt();
for &n in &he.nodes {
c_new[eid] += sw * z[n];
}
}
let norm: f64 = c_new.iter().map(|v| v * v).sum::<f64>().sqrt();
if norm == 0.0 {
return vec![0.0; e];
}
for v in &mut c_new {
*v /= norm;
}
let diff: f64 = c_new
.iter()
.zip(c.iter())
.map(|(a, b)| (a - b).abs())
.sum();
c = c_new;
if diff < tol {
break;
}
}
let total: f64 = c.iter().map(|v| v.abs()).sum();
if total > 0.0 {
c.iter().map(|v| v.abs() / total).collect()
} else {
vec![0.0; e]
}
}
pub fn hypergraph_clustering_coefficient(node: usize, hg: &IndexedHypergraph) -> f64 {
if node >= hg.n_nodes() {
return 0.0;
}
let mut neighbour_set: HashSet<usize> = HashSet::new();
if let Some(ids) = hg.hyperedges_of_node(node) {
for &eid in ids {
for &n in &hg.hyperedges[eid].nodes {
if n != node {
neighbour_set.insert(n);
}
}
}
}
let neighbours: Vec<usize> = neighbour_set.into_iter().collect();
let k = neighbours.len();
if k < 2 {
return 0.0;
}
let mut connected_pairs: usize = 0;
let total_pairs = k * (k - 1) / 2;
for i in 0..k {
for j in (i + 1)..k {
let u = neighbours[i];
let v = neighbours[j];
let u_edges: HashSet<usize> = hg
.hyperedges_of_node(u)
.unwrap_or(&[])
.iter()
.copied()
.collect();
let v_edges = hg.hyperedges_of_node(v).unwrap_or(&[]);
if v_edges.iter().any(|id| u_edges.contains(id)) {
connected_pairs += 1;
}
}
}
connected_pairs as f64 / total_pairs as f64
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn triangle_hg() -> IndexedHypergraph {
let mut hg = IndexedHypergraph::new(3);
hg.add_hyperedge(vec![0, 1], 1.0).expect("add ok");
hg.add_hyperedge(vec![1, 2], 1.0).expect("add ok");
hg.add_hyperedge(vec![0, 2], 1.0).expect("add ok");
hg
}
#[test]
fn test_hyperedge_dedup() {
let he = Hyperedge::new(0, vec![3, 1, 2, 1], 1.0);
assert_eq!(he.nodes, vec![1, 2, 3]);
}
#[test]
fn test_hyperedge_contains() {
let he = Hyperedge::new(0, vec![0, 2, 4], 1.0);
assert!(he.contains(0));
assert!(!he.contains(1));
}
#[test]
fn test_intersection_size() {
let a = Hyperedge::new(0, vec![0, 1, 2], 1.0);
let b = Hyperedge::new(1, vec![1, 2, 3], 1.0);
assert_eq!(a.intersection_size(&b), 2);
}
#[test]
fn test_add_hyperedge_invalid_node() {
let mut hg = IndexedHypergraph::new(3);
assert!(hg.add_hyperedge(vec![0, 5], 1.0).is_err());
}
#[test]
fn test_add_hyperedge_negative_weight() {
let mut hg = IndexedHypergraph::new(3);
assert!(hg.add_hyperedge(vec![0, 1], -1.0).is_err());
}
#[test]
fn test_degree_and_weighted_degree() {
let mut hg = IndexedHypergraph::new(3);
hg.add_hyperedge(vec![0, 1], 2.0).expect("ok");
hg.add_hyperedge(vec![0, 2], 3.0).expect("ok");
assert_eq!(hg.degree(0), 2);
assert_relative_eq!(hg.weighted_degree(0), 5.0, epsilon = 1e-10);
}
#[test]
fn test_incidence_matrix_shape() {
let hg = triangle_hg();
assert_eq!(hg.incidence_matrix().shape(), &[3, 3]);
}
#[test]
fn test_clique_expansion_triangle() {
let hg = triangle_hg();
let g = clique_expansion(&hg);
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 3);
}
#[test]
fn test_star_expansion_node_count() {
let hg = triangle_hg(); let g = hg.star_expansion();
assert_eq!(g.node_count(), 6);
assert_eq!(g.edge_count(), 6);
}
#[test]
fn test_bipartite_representation() {
let mut hg = IndexedHypergraph::new(3);
hg.add_hyperedge(vec![0, 1], 1.0).expect("ok");
let bg = hg.bipartite_representation();
assert_eq!(bg.node_count(), 4);
}
#[test]
fn test_generic_hypergraph_degree() {
let mut hg: Hypergraph<&str, &str> = Hypergraph::new();
let a = hg.add_node("a");
let b = hg.add_node("b");
let c = hg.add_node("c");
hg.add_hyperedge("e1", vec![a, b, c]);
hg.add_hyperedge("e2", vec![a, b]);
assert_eq!(hg.node_degree(a), 2);
assert_eq!(hg.node_degree(c), 1);
}
#[test]
fn test_generic_try_add_hyperedge_oob() {
let mut hg: Hypergraph<i32, i32> = Hypergraph::new();
hg.add_node(0);
assert!(hg.try_add_hyperedge(99, vec![0, 5]).is_err());
}
#[test]
fn test_line_graph_triangle() {
let hg = triangle_hg();
let lg = line_graph(&hg);
assert_eq!(lg.node_count(), 3);
assert_eq!(lg.edge_count(), 3);
}
#[test]
fn test_random_walk_length() {
let hg = triangle_hg();
let path = hypergraph_random_walk_seeded(&hg, 0, 10, 99).expect("ok");
assert_eq!(path.len(), 11);
}
#[test]
fn test_hyperedge_centrality_sums_to_one() {
let hg = triangle_hg();
let c = hyperedge_centrality(&hg);
let sum: f64 = c.iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
}
#[test]
fn test_clustering_coeff_full_triangle() {
let hg = triangle_hg();
assert_relative_eq!(
hypergraph_clustering_coefficient(0, &hg),
1.0,
epsilon = 1e-10
);
}
}