Source code for cfpq_data.graphs.readwrite.mtx

"""Read (and write) a graph from (and to) a directory of MatrixMarket files."""
import logging
import pathlib
from typing import Dict, List, Tuple, Union

import networkx as nx

__all__ = [
    "filename_to_label",
    "label_to_filename",
    "graph_from_mtx_dir",
    "graph_to_mtx_dir",
]

#: The header lines of the Boolean MatrixMarket files of the dataset.
_MTX_HEADER = (
    "%%MatrixMarket matrix coordinate pattern general",
    "%%GraphBLAS type bool",
)


[docs] def filename_to_label(filename: Union[pathlib.Path, str]) -> str: """Returns the edge label of a MatrixMarket file name. The file name is the label itself (``load_5.mtx`` holds the edges labeled ``load_5``). Parameters ---------- filename : Union[Path, str] The name of a MatrixMarket file. Examples -------- >>> filename_to_label("type.mtx") 'type' >>> filename_to_label("alloc_r.mtx") 'alloc_r' >>> filename_to_label("load_5.mtx") 'load_5' Returns ------- label : str The edge label of the file. """ label = pathlib.Path(filename).stem logging.info(f"Map {filename=} to {label=}") return label
[docs] def label_to_filename(label: str) -> str: """Returns the canonical MatrixMarket file name of an edge label. The canonical style is ``<label>.mtx``; reading it back with :func:`filename_to_label` returns the same label. Parameters ---------- label : str The edge label. Examples -------- >>> label_to_filename("load_5") 'load_5.mtx' >>> label_to_filename("alloc_r") 'alloc_r.mtx' Returns ------- filename : str The file name of the label. """ return f"{label}.mtx"
[docs] def graph_from_mtx_dir(path: Union[pathlib.Path, str]) -> nx.MultiDiGraph: """Loads a graph from a directory of MatrixMarket files. Each ``*.mtx`` file in the directory is a Boolean pattern matrix with one entry per edge of a single label (0-based indices, no values); the label is derived from the file name by :func:`filename_to_label`. Parameters ---------- path : Union[Path, str] The path to the directory with the MatrixMarket files. Examples -------- >>> import pathlib, tempfile >>> d = pathlib.Path(tempfile.mkdtemp()) / "graph" >>> _ = d.mkdir(parents=True) >>> _ = (d / "a.mtx").write_text( ... "%%MatrixMarket matrix coordinate pattern general\\n" ... "%%GraphBLAS type bool\\n3 3 2\\n0 1\\n1 2\\n" ... ) >>> _ = (d / "b_5.mtx").write_text( ... "%%MatrixMarket matrix coordinate pattern general\\n" ... "%%GraphBLAS type bool\\n3 3 1\\n2 0\\n" ... ) >>> g = graph_from_mtx_dir(d) >>> sorted((u, v, e["label"]) for u, v, e in g.edges(data=True)) [(0, 1, 'a'), (1, 2, 'a'), (2, 0, 'b_5')] Returns ------- g : MultiDiGraph Loaded graph. """ path = pathlib.Path(path) graph = nx.MultiDiGraph() for mtx_file in sorted(path.glob("*.mtx")): label = filename_to_label(mtx_file.name) with open(mtx_file, "r") as f: lines = [line.strip() for line in f if line.strip()] if tuple(lines[:2]) != _MTX_HEADER: raise ValueError(f"Unexpected header in {mtx_file=}") rows, cols, nnz = map(int, lines[2].split()) pairs = lines[3:] if len(pairs) != nnz: raise ValueError(f"{mtx_file=} declares {nnz} entries but has {len(pairs)}") for pair in pairs: i, j = map(int, pair.split()) graph.add_edge(i, j, label=label) logging.info(f"Load {graph=} from {path=}") return graph
[docs] def graph_to_mtx_dir( graph: nx.MultiDiGraph, path: Union[pathlib.Path, str] ) -> pathlib.Path: """Saves the ``graph`` to a directory of MatrixMarket files by ``path``. One file per edge label (the canonical name from :func:`label_to_filename`); each file is a Boolean pattern matrix with 0-based indices and no values. The node ids must be non-negative integers (the matrix dimensions are the largest node id plus one). Parameters ---------- graph : MultiDiGraph Graph to save. path : Union[Path, str] The path to the directory where the MatrixMarket files will be saved. Examples -------- >>> import pathlib, tempfile >>> g = nx.MultiDiGraph() >>> _ = g.add_edges_from( ... [(0, 1, {"label": "a"}), (1, 2, {"label": "a"}), ... (2, 0, {"label": "b_5"})] ... ) >>> d = pathlib.Path(tempfile.mkdtemp()) >>> path = graph_to_mtx_dir(g, d / "graph") >>> g2 = graph_from_mtx_dir(path) >>> sorted((u, v, e["label"]) for u, v, e in g2.edges(data=True)) == \\ ... sorted((u, v, e["label"]) for u, v, e in g.edges(data=True)) True Returns ------- path : Path Path to the directory where the graph will be saved. """ dest = pathlib.Path(path) dest.mkdir(parents=True, exist_ok=True) nodes = list(graph.nodes()) if any(not isinstance(node, int) or node < 0 for node in nodes): raise TypeError(f"The node ids of {graph=} must be non-negative integers") dimension = max(nodes, default=-1) + 1 by_label: Dict[str, List[Tuple[int, int]]] = {} for u, v, data in graph.edges(data=True): by_label.setdefault(data["label"], []).append((u, v)) for label in sorted(by_label): pairs = by_label[label] lines = [*_MTX_HEADER, f"{dimension} {dimension} {len(pairs)}"] lines += [f"{i} {j}" for i, j in pairs] (dest / label_to_filename(label)).write_text("\n".join(lines) + "\n") dest = dest.resolve() logging.info(f"Save {graph=} to {dest=}") return dest