#!/usr/bin/env python3 """Generate values.tex from plugins under analysis/values/. Each plugin exposes compute(derived) -> (keys, sources). Keys are merged into one \\pgfkeys block; sources are recorded in the header for provenance. """ import argparse import datetime as dt import hashlib import importlib.util from pathlib import Path from types import ModuleType def load_plugins(plugin_dir: Path, skip: set[str]) -> list[tuple[str, ModuleType]]: plugins: list[tuple[str, ModuleType]] = [] for p in sorted(plugin_dir.glob("*.py")): if p.name.startswith("_") or p.stem in skip: continue spec = importlib.util.spec_from_file_location(f"values.{p.stem}", p) if spec is None or spec.loader is None: raise SystemExit(f"could not load plugin {p}") mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) if hasattr(mod, "compute"): plugins.append((p.stem, mod)) return plugins def file_mtime_iso(p: Path) -> str: return dt.datetime.fromtimestamp(p.stat().st_mtime).isoformat(timespec="seconds") def file_sha256(p: Path) -> str: h = hashlib.sha256() with p.open("rb") as f: for chunk in iter(lambda: f.read(1 << 16), b""): h.update(chunk) return h.hexdigest() # Map key-name suffix to siunitx unit. UNIT_SUFFIXES = { "-us": r"\micro\second", "-ms": r"\milli\second", "-gbps": r"\giga\bit\per\second", "-pct": r"\percent", "-kb": r"\kilo\byte", } NUM_SUFFIXES = ("-p", "-alpha", "-n", "-uncorrected") NUM_PREFIXES = ("n-",) def value_to_latex(key: str, value: str) -> str: leaf = key.rsplit("/", 1)[-1] for suffix, unit in UNIT_SUFFIXES.items(): if leaf.endswith(suffix): return f"\\qty{{{value}}}{{{unit}}}" if any(leaf.startswith(p) for p in NUM_PREFIXES) or any(leaf.endswith(s) for s in NUM_SUFFIXES): return f"\\num{{{value}}}" return value def format_pgfkeys(all_keys: dict[str, str], provenance: list[str]) -> str: lines: list[str] = [] lines.append("% Auto-generated by analysis/gen_values.py. Do not edit by hand.") lines.append(f"% Generated: {dt.datetime.now().isoformat(timespec='seconds')}") lines.extend(provenance) for k, v in sorted(all_keys.items()): lines.append(f"\\pgfkeyssetvalue{{/{k}}}{{{value_to_latex(k, v)}}}") return "\n".join(lines) + "\n" def main() -> None: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--derived", required=True, type=Path, help="derived/ root with per-experiment subdirs") p.add_argument("--out", required=True, type=Path, help="output values.tex path") p.add_argument("--plugins", type=Path, default=Path(__file__).parent / "values", help="plugin directory (default: analysis/values)") p.add_argument("--skip", nargs="*", default=[], help="plugin names to skip (stem only, e.g. --skip cpu rtt)") args = p.parse_args() plugins = load_plugins(args.plugins, set(args.skip)) if not plugins: raise SystemExit(f"no plugins found in {args.plugins} (after --skip)") all_keys: dict[str, str] = {} provenance: list[str] = ["% Plugins:"] for name, mod in plugins: keys, sources = mod.compute(args.derived) dup = set(keys) & set(all_keys) if dup: raise SystemExit(f"plugin {name} duplicates keys: {sorted(dup)}") all_keys.update(keys) provenance.append(f"% {name}: {len(keys)} keys") for src in sources: provenance.append(f"% {src} (mtime {file_mtime_iso(src)}, sha256 {file_sha256(src)})") print(f" {name}: {len(keys)} keys from {len(sources)} sources") args.out.parent.mkdir(parents=True, exist_ok=True) args.out.write_text(format_pgfkeys(all_keys, provenance)) print(f"wrote {args.out} ({len(all_keys)} keys total)") if __name__ == "__main__": main()