Source code for pyfracval.catalog

"""Load and filter a ``cluster_index.csv`` master index of generated aggregates.

``cluster_index.csv`` is written by ``scripts/generate_cluster_data.py`` as a
flat table -- one row per generated ``.dat`` file, with the generation
parameters (sigma/Df/N/kf/...) that would otherwise only be recoverable by
parsing directory names or re-reading every file's YAML header via
``pyfracval.schemas.Metadata.from_file()``. This module is the native way to
query that table instead: no pandas dependency (only an optional/plot
dependency of this package, not a core one), just the stdlib ``csv`` module
and a small pydantic model (``pyfracval.schemas.ClusterEntry``).
"""

from __future__ import annotations

import csv
from collections.abc import Iterable
from pathlib import Path

from pyfracval.schemas import ClusterEntry


[docs] def load_catalog( index_path: str | Path, data_root: str | Path | None = None, ) -> list[ClusterEntry]: """Load every row of a ``cluster_index.csv`` as a list of ``ClusterEntry``. *index_path*'s ``filepath`` column holds an absolute path from wherever the data was originally generated -- not portable across machines or repos that keep their own copy of the same aggregate files. When *data_root* is given, each entry's ``filepath`` is re-based under it instead of trusting the stored absolute path: the last 3 path components (``<config>/<sigma_..__Df_..__N_..>/<filename>.dat``) are the part of the layout ``scripts/generate_cluster_data.py`` actually guarantees, so re-joining just those under *data_root* is reliable regardless of where the CSV says the file used to live. When *data_root* is ``None``, ``filepath`` is used as-is (verbatim from the CSV). """ index_path = Path(index_path) data_root = Path(data_root) if data_root is not None else None entries: list[ClusterEntry] = [] with open(index_path, newline="", encoding="utf-8") as f: for row in csv.DictReader(f): filepath = Path(row["filepath"]) if data_root is not None: filepath = data_root.joinpath(*filepath.parts[-3:]) entries.append( ClusterEntry( config=row["config"], sigma=float(row["sigma"]), Df=float(row["Df"]), N=int(row["N"]), kf=float(row["kf"]), attempt=int(row["attempt"]), seed=int(row["seed"]), success=row["success"].strip().lower() == "true", filepath=filepath, ) ) return entries
def _matches(value: float | int | str, allowed: object) -> bool: if allowed is None: return True if isinstance(allowed, (list, tuple, set, frozenset)): return value in allowed return value == allowed
[docs] def filter_catalog( entries: Iterable[ClusterEntry], *, sigma: float | Iterable[float] | None = None, Df: float | Iterable[float] | None = None, N: int | Iterable[int] | None = None, kf: float | Iterable[float] | None = None, config: str | Iterable[str] | None = None, success_only: bool = True, ) -> list[ClusterEntry]: """Filter catalog entries by generation parameters. Each keyword accepts either a single value (exact match) or an iterable of allowed values; ``None`` (the default for all but *success_only*) means "don't filter on this field". *success_only* (default ``True``) drops any entry with ``success=False`` before applying the other filters -- a failed generation attempt has no usable geometry. """ result = [] for entry in entries: if success_only and not entry.success: continue if not _matches(entry.sigma, sigma): continue if not _matches(entry.Df, Df): continue if not _matches(entry.N, N): continue if not _matches(entry.kf, kf): continue if not _matches(entry.config, config): continue result.append(entry) return result