"""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))