Source code for modelly.space.networks.base
"""Node rows shared by the temporal and multilayer networks.
Nodes are any hashable labels; each gets an integer row, in insertion order, so the
network's adjacency is a sparse matrix whose rows line up with a per-node numpy vector
(an agent column, a state array). Edges live in dictionaries keyed by rows, so adding or
removing one edge costs O(1) and a neighbour query costs O(degree).
"""
from __future__ import annotations
from collections.abc import Hashable, Iterable
from typing import TYPE_CHECKING
import numpy as np
if TYPE_CHECKING:
from scipy.sparse import csr_matrix
[docs]
class NodeRows:
"""Labels to rows and back, for a network over ``nodes``."""
def _init_nodes(self, nodes: int | Iterable[Hashable]) -> None:
"""``nodes`` is a count (labels ``0 .. n-1``) or an iterable of distinct labels."""
self.nodes: list[Hashable] = (
list(range(nodes)) if isinstance(nodes, int) else list(nodes)
)
self._row: dict[Hashable, int] = {node: i for i, node in enumerate(self.nodes)}
if len(self._row) != len(self.nodes):
raise ValueError("Node labels must be distinct.")
def __len__(self) -> int:
"""Return the number of nodes."""
return len(self.nodes)
def __contains__(self, node: Hashable) -> bool:
"""Whether ``node`` is a node of the network."""
return node in self._row
[docs]
def add_node(self, node: Hashable) -> int:
"""Add ``node`` if it is new, and return its row."""
row = self._row.get(node)
if row is None:
row = len(self.nodes)
self._row[node] = row
self.nodes.append(node)
self._on_new_node(row)
return row
def _on_new_node(self, row: int) -> None:
"""Hook for subclasses that keep per-row storage."""
[docs]
def row(self, node: Hashable) -> int:
"""The row of ``node``."""
try:
return self._row[node]
except KeyError as e:
raise KeyError(f"{node!r} is not a node of this network.") from e
[docs]
def rows(self, nodes: Iterable[Hashable]) -> np.ndarray:
"""The rows of ``nodes``."""
return np.fromiter((self.row(node) for node in nodes), dtype=np.intp)
[docs]
def csr_from_rows(
rows: dict[int, dict[int, float]], n: int, scale: float = 1.0
) -> csr_matrix:
"""A CSR matrix with entry ``scale * rows[i][j]`` for every stored pair."""
from scipy.sparse import csr_matrix # noqa: PLC0415 - scipy is heavy
degrees = np.zeros(n, dtype=np.intp)
for i, targets in rows.items():
degrees[i] = len(targets)
indptr = np.concatenate(([0], np.cumsum(degrees)))
indices = np.empty(indptr[-1], dtype=np.intp)
data = np.empty(indptr[-1], dtype=float)
for i, targets in rows.items():
start = indptr[i]
indices[start : start + len(targets)] = list(targets)
data[start : start + len(targets)] = list(targets.values())
matrix = csr_matrix((data * scale, indices, indptr), shape=(n, n))
matrix.sort_indices()
return matrix