# -*- coding: utf-8 -*-
"""
hypernetwork.py
High-performance Hypernetwork using dict-of-sets representation.
"""
from typing import Dict, List, Set, Tuple
[docs]
class Hypernetwork:
"""
Hypernetwork implemented with two dicts:
- nodes_map: node_id -> set of edge_ids
- edges_map: edge_id -> set of node_ids
All operations are optimized to O(1) amortized where possible.
"""
[docs]
def __init__(self) -> None:
self.nodes_map: Dict[int, Set[int]] = {}
self.edges_map: Dict[int, Set[int]] = {}
[docs]
def lightweight_copy(self) -> 'Hypernetwork':
"""
Create a lightweight copy of the hypernetwork.
Returns:
A new Hypernetwork instance with copied structure
"""
new_hn = Hypernetwork()
new_hn.nodes_map = {
nid: edges.copy() for nid, edges in self.nodes_map.items()
}
new_hn.edges_map = {
eid: nodes.copy() for eid, nodes in self.edges_map.items()
}
return new_hn
[docs]
def add_node(self, node_id: int) -> None:
"""Add a node if not present."""
self.nodes_map.setdefault(node_id, set())
[docs]
def remove_node(self, node_id: int) -> None:
"""Remove a node and its incidences."""
edge_ids = self.nodes_map.pop(node_id, None)
if not edge_ids:
return
for eid in list(edge_ids):
self.edges_map[eid].discard(node_id)
if not self.edges_map[eid]:
self.edges_map.pop(eid, None)
[docs]
def add_hyperedge(self, edge_id: int, members: List[int]) -> None:
"""Add a hyperedge linking given node IDs."""
if edge_id in self.edges_map:
return
member_set = set(members)
self.edges_map[edge_id] = member_set
for nid in member_set:
self.nodes_map.setdefault(nid, set()).add(edge_id)
[docs]
def add_node_to_hyperedge(self, edge_id: int, node_id: int) -> None:
"""Add a node to an existing hyperedge."""
if edge_id not in self.edges_map:
return
if node_id in self.edges_map[edge_id]:
return
self.edges_map[edge_id].add(node_id)
self.nodes_map.setdefault(node_id, set()).add(edge_id)
[docs]
def remove_node_from_hyperedge(self, edge_id: int, node_id: int) -> None:
"""Remove a node from a given hyperedge."""
if edge_id not in self.edges_map:
return
if node_id not in self.edges_map[edge_id]:
return
self.edges_map[edge_id].remove(node_id)
self.nodes_map[node_id].remove(edge_id)
if not self.edges_map[edge_id]:
self.edges_map.pop(edge_id, None)
if not self.nodes_map.get(node_id):
self.nodes_map.pop(node_id, None)
[docs]
def remove_hyperedge(self, edge_id: int) -> None:
"""Remove a hyperedge and its incidences (nodes remain)."""
node_ids = self.edges_map.pop(edge_id, None)
if not node_ids:
return
for nid in node_ids:
self.nodes_map[nid].discard(edge_id)
[docs]
def get_nodes(self, edge_id: int) -> List[int]:
"""List node IDs in a given hyperedge."""
return list(self.edges_map.get(edge_id, []))
[docs]
def get_hyperedges(self, node_id: int) -> List[int]:
"""List hyperedge IDs containing a given node."""
return list(self.nodes_map.get(node_id, []))
[docs]
def get_neighbors(self, node_id: int) -> List[int]:
"""List neighbors of a given node."""
neighbors: Set[int] = set()
for eid in self.nodes_map.get(node_id, []):
neighbors |= (self.edges_map[eid] - {node_id})
return list(neighbors)
[docs]
def degree(self, node_id: int) -> int:
"""Return degree (number of neighbors) of a node."""
return len(self.get_neighbors(node_id))
[docs]
def hyperdegree(self, node_id: int) -> int:
"""Return hyperdegree (number of hyperedges) of a node."""
return len(self.nodes_map.get(node_id, []))
[docs]
def order(self) -> int:
"""Return number of nodes."""
return len(self.nodes_map)
[docs]
def size(self) -> int:
"""Return number of hyperedges."""
return len(self.edges_map)
[docs]
def avg_deg(self) -> float:
"""Return average node degree."""
n = self.order()
return 0.0 if n == 0 else sum(
self.degree(nid) for nid in self.nodes_map) / n
[docs]
def avg_hyperdegree(self) -> float:
"""Return average hyperdegree of nodes."""
n = self.order()
return 0.0 if n == 0 else sum(
self.hyperdegree(nid) for nid in self.nodes_map) / n
[docs]
def hyperedge_size(self, edge_id: int) -> int:
"""Return size (number of nodes) of a hyperedge."""
return len(self.edges_map.get(edge_id, []))
[docs]
def avg_hyperedge_size(self) -> float:
"""Return average hyperedge size."""
m = self.size()
return 0.0 if m == 0 else sum(
self.hyperedge_size(eid) for eid in self.edges_map) / m
[docs]
def line_graph(self) -> Tuple[List[int], List[Tuple[int, int]]]:
"""Return line graph (edge_ids, intersecting hyperedges)."""
edge_ids = list(self.edges_map)
lg: List[Tuple[int, int]] = []
for i, e1 in enumerate(edge_ids):
for e2 in edge_ids[i + 1:]:
if self.edges_map[e1] & self.edges_map[e2]:
lg.append((e1, e2))
return edge_ids, lg
[docs]
def print_info(self) -> None:
"""Print basic hypernetwork metrics."""
print(f"Order: {self.order()}")
print(f"Size: {self.size()}")
print(f"Avg degree: {self.avg_deg():.2f}")
print(f"Avg hyperdegree: {self.avg_hyperdegree():.2f}")
print(f"Avg hyperedge size: {self.avg_hyperedge_size():.2f}")
@property
def nodes(self) -> Dict[int, Set[int]]:
"""Expose mapping of node IDs to hyperedge sets."""
return self.nodes_map
@property
def edges(self) -> Dict[int, Set[int]]:
"""Expose mapping of hyperedge IDs to node sets."""
return self.edges_map