Skip to content

Commit 3bec62c

Browse files
wmaynerclaude
andcommitted
Merge provenance-writers: script-facing provenance writers
provenance.save_json/save_npz/save_dataframe encode parameters in the filename, never overwrite existing files, and embed a full Provenance record; read_metadata reads it back from any of the three formats. The three benchmark scripts with hand-rolled copies now use the shared helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEAxNzhDCaTrntX3o1JqMV
2 parents 0a0cabc + c76322d commit 3bec62c

9 files changed

Lines changed: 1230 additions & 34 deletions

File tree

ROADMAP.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -633,9 +633,13 @@ extending B1's bound-certificate assertions.
633633
is gone. Spec: `docs/superpowers/specs/2026-07-19-result-serialization-design.md`.
634634
- **Thread-backend progress (M13).** The thread backend discards the progress policy on entry
635635
(an in-code deferral note); hook the progress bar into its futures loop.
636-
- **Script-facing provenance writer (M14).** `provenance.save_json`/`save_npz` with git SHA,
637-
parameters encoded in the filename, and no-clobber versioning — consolidating the pattern
638-
that experiment scripts repeatedly re-implement.
636+
- **Script-facing provenance writer (M14).** *Landed 2026-07-20:*
637+
`provenance.save_json`/`save_npz`/`save_dataframe` (parquet, per the DataFrame-output
638+
convention) with `format_stem` filename encoding, `unique_path` no-clobber versioning, an
639+
embedded `Provenance` record, and `read_metadata` to read it back; the three benchmark
640+
scripts with hand-rolled copies now import the shared helpers (the cross-generation
641+
harness keeps a fallback for the pre-refactor side). Spec:
642+
`docs/superpowers/specs/2026-07-20-provenance-writers-design.md`.
639643

640644
Already tracked elsewhere (verified during the triage): the Wave 7 ii-gate (M4), the
641645
intervention/lesion surface (N14 ← M8), the Φ-structure distance surface (N16 ← M6), the

benchmarks/b18_dispatch_gate.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666

6767
import numpy as np
6868

69+
from pyphi.provenance import unique_path
70+
6971
RESULTS_DIR = Path(__file__).parent / "b18_dispatch_gate_results"
7072

7173
# Workload sizes as fractions of the level's chunksize. Sizes at or below
@@ -642,15 +644,6 @@ def run_level(name: str, seed: int, trials: int) -> dict:
642644
}
643645

644646

645-
def unique_path(directory: Path, stem: str, suffix: str) -> Path:
646-
path = directory / f"{stem}{suffix}"
647-
version = 2
648-
while path.exists():
649-
path = directory / f"{stem}_v{version}{suffix}"
650-
version += 1
651-
return path
652-
653-
654647
def main() -> None:
655648
parser = argparse.ArgumentParser(description=__doc__)
656649
parser.add_argument("--seed", type=int, default=1810)

benchmarks/iit_3_vs_4/harness.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -507,18 +507,22 @@ def extract_phase_times(pstats_path: Path) -> dict[str, float]:
507507
return out
508508

509509

510-
def unique_path(directory: Path, stem: str, suffix: str) -> Path:
511-
"""Return a non-clobbering path: stem.suffix, then stem_v2.suffix, ..."""
512-
directory.mkdir(parents=True, exist_ok=True)
513-
base = directory / f"{stem}{suffix}"
514-
if not base.exists():
515-
return base
516-
n = 2
517-
while True:
518-
candidate = directory / f"{stem}_v{n}{suffix}"
519-
if not candidate.exists():
520-
return candidate
521-
n += 1
510+
try:
511+
from pyphi.provenance import unique_path
512+
except ImportError:
513+
# The pre-refactor generation has no pyphi.provenance; keep a local copy.
514+
def unique_path(directory: Path, stem: str, suffix: str) -> Path:
515+
"""Return a non-clobbering path: stem.suffix, then stem_v2.suffix, ..."""
516+
directory.mkdir(parents=True, exist_ok=True)
517+
base = directory / f"{stem}{suffix}"
518+
if not base.exists():
519+
return base
520+
n = 2
521+
while True:
522+
candidate = directory / f"{stem}_v{n}{suffix}"
523+
if not candidate.exists():
524+
return candidate
525+
n += 1
522526

523527

524528
def write_record(record: dict[str, Any]) -> Path:

benchmarks/iit_3_vs_4/p18_inversion_share.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535

3636
import numpy as np
3737

38+
from pyphi.provenance import format_stem
39+
from pyphi.provenance import unique_path
40+
3841
CORE_N = 6
3942
CORE_DENSITY = 0.35
4043
COUPLING_MEAN = 1.0
@@ -112,15 +115,11 @@ def _profile_sia(system) -> dict:
112115

113116
def _output_path(seed: int, run_label: str | None) -> Path:
114117
results_dir = Path(__file__).parent / "results"
115-
results_dir.mkdir(exist_ok=True)
116-
label = f"_{run_label}" if run_label else ""
117-
base = f"p18_inversion_share_seed{seed}{label}"
118-
path = results_dir / f"{base}.json"
119-
version = 2
120-
while path.exists():
121-
path = results_dir / f"{base}_v{version}.json"
122-
version += 1
123-
return path
118+
return unique_path(
119+
results_dir,
120+
format_stem("p18_inversion_share", {"seed": seed}, run_label),
121+
".json",
122+
)
124123

125124

126125
def main() -> None:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added script-facing provenance writers: `pyphi.provenance.save_json`, `save_npz`, and `save_dataframe` (parquet) encode parameters in the filename, never overwrite existing files (`_v2`/`_v3` versioning), and embed a full `Provenance` record (git SHA, seed, versions) in every output; `read_metadata` reads it back from any of the three formats.

docs/howto/save-load.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,44 @@ except TypeError as error:
133133
print(error)
134134
```
135135

136+
## Experiment provenance writers
137+
138+
Experiment scripts need two things beyond `pyphi.save`: output files that
139+
never overwrite earlier runs, and a record of how each file was produced.
140+
The writers in `pyphi.provenance` provide both. Parameters are encoded
141+
into the filename, a repeated save lands in a `_v2` file instead of
142+
clobbering the first, and every file embeds a full provenance record —
143+
pyphi version, git commit, timestamp, and seed.
144+
145+
```{code-cell} python
146+
from pyphi import provenance
147+
148+
path = provenance.save_json(
149+
{"phi": 0.133873},
150+
out,
151+
"sweep_study",
152+
params={"seed": 42, "trials": 60},
153+
)
154+
path.name
155+
```
156+
157+
```{code-cell} python
158+
provenance.save_json(
159+
{"phi": 0.5}, out, "sweep_study", params={"seed": 42, "trials": 60}
160+
).name
161+
```
162+
163+
`save_npz` does the same for arrays of raw per-trial data, and
164+
`save_dataframe` writes a DataFrame as parquet — the format used for
165+
DataFrame outputs throughout PyPhi — with the metadata embedded in the
166+
parquet schema. `read_metadata` recovers the provenance and parameters
167+
from any of the three formats:
168+
169+
```{code-cell} python
170+
metadata = provenance.read_metadata(path)
171+
{key: metadata["provenance"][key] for key in ("seed", "pyphi_version")}
172+
```
173+
136174
## Compatibility note
137175

138176
This serializer is a deliberate format break from the `jsonify` layer used in

0 commit comments

Comments
 (0)