Source code for digraphx.csr_digraph

"""
CSRDiGraph — Memory‑efficient directed graph backed by Compressed Sparse Row storage.

For a graph with *N* nodes and *E* edges the internal arrays use
O(N + E) machine integers plus one Python object per edge (the data dict).
This is dramatically smaller than ``dict``‑of‑``dict`` storage when *N* is large
and the graph is sparse.
"""

from array import array
from typing import Mapping

__all__ = ["CSRDiGraph"]


class _CSRNeighbors(Mapping):
    """Read‑only view into a single node's adjacency (backed by CSR arrays)."""

    __slots__ = ("_indices", "_data", "_start", "_end")

    def __init__(self, indices: array, data: list, start: int, end: int) -> None:
        self._indices = indices
        self._data = data
        self._start = start
        self._end = end

    def __getitem__(self, v: int):
        for i in range(self._start, self._end):
            if self._indices[i] == v:
                return self._data[i]
        raise KeyError(v)

    def __iter__(self):
        return iter(self._indices[self._start : self._end])

    def __len__(self):
        return self._end - self._start

    def __contains__(self, v):
        return any(self._indices[i] == v for i in range(self._start, self._end))

    def items(self):
        for i in range(self._start, self._end):
            yield self._indices[i], self._data[i]

    def keys(self):
        return iter(self._indices[self._start : self._end])

    def values(self):
        return iter(self._data[self._start : self._end])


[docs] class CSRDiGraph(Mapping): """Memory‑efficient directed graph using Compressed Sparse Row storage. Nodes must be integers ``0 .. N-1``. Call :meth:`init_nodes` first, then :meth:`add_edge` any number of times, then call :meth:`freeze` to compact storage. After freezing the graph is still fully readable but no longer supports edge addition. Examples -------- >>> g = CSRDiGraph() >>> g.init_nodes(3) >>> g.add_edge(0, 1, weight=7) >>> g.add_edge(1, 2, weight=3) >>> g.add_edge(2, 0, weight=-5) >>> g.freeze() >>> sorted(g[0].items()) [(1, {'weight': 7})] >>> list(g.nodes()) [0, 1, 2] """ def __init__(self) -> None: self._num_nodes = 0 self._edges: list[list] | None = None # temp storage before freeze self._indptr: array | None = None self._indices: array | None = None self._data: list | None = None self._frozen = False # ---- construction ----
[docs] def init_nodes(self, num_nodes: int) -> None: """Allocate storage for *num_nodes* (``0 … num_nodes-1``).""" self._num_nodes = num_nodes self._edges = [[] for _ in range(num_nodes)] self._frozen = False
[docs] def add_edge(self, u: int, v: int, **attr) -> None: # type: ignore """Add a directed edge ``u → v`` with optional attributes.""" assert not self._frozen, "graph is frozen" assert self._edges is not None self._edges[u].append((v, attr if attr else {}))
[docs] def freeze(self) -> None: """Compact internal storage to CSR arrays. After this call the graph is read‑only. """ if self._frozen: return assert self._edges is not None indptr = array("i", [0]) # cumulative counts for node_edges in self._edges: indptr.append(indptr[-1] + len(node_edges)) total = indptr[-1] indices = array("i", [0]) * total # placeholder data: list = [None] * total idx = 0 for node_edges in self._edges: for v, d in node_edges: indices[idx] = v data[idx] = d idx += 1 self._indptr = indptr self._indices = indices self._data = data self._edges = None # free temp storage self._frozen = True
# ---- Mapping protocol ---- def __getitem__(self, u: int): if not self._frozen: self.freeze() assert self._indptr is not None assert self._indices is not None assert self._data is not None start = self._indptr[u] end = self._indptr[u + 1] return _CSRNeighbors(self._indices, self._data, start, end) def __iter__(self): return iter(range(self._num_nodes)) def __len__(self): return self._num_nodes def __contains__(self, u): return isinstance(u, int) and 0 <= u < self._num_nodes
[docs] def items(self): if not self._frozen: self.freeze() for u in range(self._num_nodes): start = self._indptr[u] end = self._indptr[u + 1] yield u, _CSRNeighbors(self._indices, self._data, start, end)
# ---- graph-like helpers ----
[docs] def nodes(self): """Return all node identifiers.""" return range(self._num_nodes)
def __repr__(self): if self._frozen and self._indptr is not None: total = self._indptr[-1] elif self._edges is not None: total = sum(len(e) for e in self._edges) else: total = 0 return f"CSRDiGraph({self._num_nodes} nodes, {total} edges)"