Source code for orbitsi.arraygraph

"""ArrayGraph: a NetworkX graph read straight into CSR arrays (orbitsi.utils.read_graph_from_file).

It is an nx.Graph whose node and adjacency dictionaries are built only when first read; until then
GraphIndex and the C++ core use its arrays directly. On that first read it fills the dictionaries, in
the order NetworkX would have built them from the file, and becomes a plain nx.Graph.
"""
import networkx as nx


class _Deferred:
    """Stands in for nx.Graph's _adj and _node: a read builds the graph's dictionaries first."""
    def __init__(self, name):
        self.name = name
        self.setter = nx.Graph.__dict__.get(name)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        obj._materialise()
        return obj.__dict__[self.name]

    def __set__(self, obj, value):
        if self.setter is None:
            obj.__dict__[self.name] = value
        else:
            self.setter.__set__(obj, value)


[docs] class ArrayGraph(nx.Graph): """A :class:`networkx.Graph` read into arrays, whose NetworkX dictionaries are built on first use. :func:`orbitsi.utils.read_graph_from_file` returns one. Until its node or adjacency dictionaries are first read, :class:`~orbitsi.graphindex.GraphIndex` and the C++ core use its arrays directly; :meth:`number_of_nodes` and ``len()`` also work without them. Any other NetworkX use (iterating nodes or edges, ``copy()``, ``subgraph()``, mutation, NetworkX algorithms) builds them, in the order NetworkX would have built them from the same file, and the object then becomes a plain :class:`networkx.Graph`. Pickling and deep copies keep it unbuilt. Parameters ---------- incoming_graph_data : optional As for :class:`networkx.Graph`; used only when ``arrays`` is None, in which case the result is a plain :class:`networkx.Graph`. arrays : tuple of numpy.ndarray, optional ``(node_ids, labels, has_label, offsets, nbors)`` as returned by ``_orbitsi_core.read_graph``: node IDs in node order, each node's label and whether it has one, and the adjacency in CSR form over node positions (the neighbours of node ``i`` are ``nbors[offsets[i]:offsets[i + 1]]``). **attr Graph attributes, as for :class:`networkx.Graph`. """ _adj = _Deferred("_adj") _node = _Deferred("_node") def __init__(self, incoming_graph_data=None, arrays=None, **attr): if arrays is None: self.__class__ = nx.Graph nx.Graph.__init__(self, incoming_graph_data, **attr) return nx.Graph.__init__(self, **attr) self.__dict__["arrays"] = arrays
[docs] def number_of_nodes(self): """The number of nodes, without building the NetworkX dictionaries.""" return len(self.__dict__["arrays"][0])
def __len__(self): return self.number_of_nodes() def _materialise(self): """Build the node and adjacency dictionaries from the arrays and become a plain nx.Graph.""" arrays = self.__dict__.get("arrays") if arrays is None: return node_ids, labels, has_label, offsets, nbors = arrays ids = node_ids.tolist() node = {x: ({"label": lab} if has else {}) for x, lab, has in zip(ids, labels.tolist(), has_label.tolist())} rows = [{} for _ in ids] off, nb = offsets.tolist(), nbors.tolist() for i, row in enumerate(rows): xi = ids[i] for k in range(off[i], off[i + 1]): j = nb[k] row[ids[j]] = rows[j][xi] if j < i else {} self.__class__ = nx.Graph del self.__dict__["arrays"] self._node = node self._adj = dict(zip(ids, rows))