1"""Provenance block attached to every saved experimental result."""2from __future__ import annotations3import datetime as _dt, json, os, platform, subprocess, sys4from pathlib import Path567def _sysctl(key: str) -> str:8 try:9 return subprocess.run(["sysctl", "-n", key], capture_output=True, text=True, timeout=5).stdout.strip()10 except Exception: # pragma: no cover11 return ""121314def host_info() -> dict:15 info = {16 "hostname": platform.node(),17 "os": f"{platform.system()} {platform.release()} ({platform.mac_ver()[0] or platform.version()})",18 "machine": platform.machine(),19 "python": sys.version.split()[0],20 }21 if platform.system() == "Darwin":22 info["cpu"] = _sysctl("machdep.cpu.brand_string")23 info["cores"] = int(_sysctl("hw.ncpu") or 0)24 mem = _sysctl("hw.memsize")25 info["ram_gb"] = round(int(mem) / 2**30, 1) if mem else None26 else: # pragma: no cover27 info["cpu"] = platform.processor()28 info["cores"] = os.cpu_count()29 return info303132def software_versions(mods=("numpy", "scipy", "sympy", "gmpy2", "z3", "pysat", "ecdsa", "coincurve")) -> dict:33 out = {}34 for m in mods:35 try:36 mod = __import__(m)37 v = getattr(mod, "__version__", None)38 if m == "z3":39 v = mod.get_version_string()40 if m == "gmpy2":41 v = mod.version()42 out[m] = v43 except Exception:44 out[m] = None45 return out464748def record(experiment: str, params: dict, measurements, *, seed=None, extra: dict | None = None,49 out: str | os.PathLike | None = None, started: float | None = None) -> dict:50 """Build (and optionally write) a result document with full provenance."""51 doc = {52 "experiment": experiment,53 "timestamp_utc": _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"),54 "command": " ".join(sys.argv),55 "cwd": os.getcwd(),56 "seed": seed,57 "host": host_info(),58 "software": software_versions(),59 "params": params,60 "measurements": measurements,61 }62 if started is not None:63 import time64 doc["elapsed_s"] = round(time.time() - started, 3)65 if extra:66 doc.update(extra)67 if out:68 p = Path(out)69 p.parent.mkdir(parents=True, exist_ok=True)70 p.write_text(json.dumps(doc, indent=1, default=str))71 return doc72