"""Exact conversion of vertex orbit counts between induced and non-induced."""
import numpy as np
import os
[docs]
class OrbitMatrixConverter:
"""Converts vertex orbit counts between induced and non-induced.
Orbits are grouped by graphlet size: orbit 0 (an edge) is the same either way; orbits 1-3, 4-14
and 15-72 are related by the matrices for 3-, 4- and 5-vertex graphlets (non-induced = M @
induced). Each M is unit upper-triangular with integer entries, so its inverse is an integer
matrix too, and both directions are exact in int64.
"""
BLOCKS = ((3, slice(1, 4)), (4, slice(4, 15)), (5, slice(15, 73)))
_loaded = None
def __init__(self):
if OrbitMatrixConverter._loaded is None:
this_dir = os.path.dirname(__file__)
path = os.path.join(this_dir, 'orbit_conversion_matrices.npz')
with np.load(path) as f:
matrices = {int(k.split('_')[1]): np.rint(f[k]).astype(np.int64) for k in f.files}
inverses = {k: np.rint(np.linalg.inv(m.astype(np.float64))).astype(np.int64)
for k, m in matrices.items()}
for k, m in matrices.items():
assert np.array_equal(m @ inverses[k], np.eye(len(m), dtype=np.int64)), k
OrbitMatrixConverter._loaded = (matrices, inverses)
self.matrices, self.inverses = OrbitMatrixConverter._loaded
def _convert(self, matrix: np.ndarray, mats: dict) -> np.ndarray:
"""Apply each graphlet size's matrix in mats to its block of orbit columns."""
matrix = np.asarray(matrix, dtype=np.int64)
if matrix.ndim != 2 or matrix.shape[1] not in (15, 73):
raise ValueError("Unsupported orbit count size for conversion (expected 15 or 73 orbits per vertex).")
out = matrix.copy()
for size, cols in self.BLOCKS:
if cols.stop <= matrix.shape[1]:
out[:, cols] = matrix[:, cols] @ mats[size].T
return out
[docs]
def noninduced_to_induced(self, noninduced_matrix: np.ndarray) -> np.ndarray:
"""Convert non-induced orbit counts (one row per vertex) to induced counts."""
return self._convert(noninduced_matrix, self.inverses)
[docs]
def induced_to_noninduced(self, induced_matrix: np.ndarray) -> np.ndarray:
"""Convert induced orbit counts (one row per vertex) to non-induced counts."""
return self._convert(induced_matrix, self.matrices)