feat(drl): publish writing-ready documentation and training tables
Add indexed DRL pinball documentation, provenance-aware training CSV export, first-phase flow rendering, and audited reproduction assets so the retained results are ready for manuscript analysis. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -37,6 +38,15 @@ LEGACY_VORTEX = (
|
||||
"vortex_taylor_y000", "vortex_taylor_yp1L", "vortex_taylor_yp2L",
|
||||
)
|
||||
LEGACY_GROUPS = LEGACY_PERIODIC + ("steady",) + LEGACY_VORTEX + ("erase",)
|
||||
SR_TRAINING = LEGACY_PERIODIC
|
||||
SR_GENERALIZATION = (
|
||||
"karman_re25", "karman_re70", "karman_re150", "karman_re300",
|
||||
"illusion_05L", "illusion_06L", "illusion_08L", "illusion_12L", "illusion_2L",
|
||||
)
|
||||
SR_VARIANT_CASE = {
|
||||
"k_front0": "karman_re100", "k_rear0": "karman_re100", "k_rear1": "karman_re100",
|
||||
"i_front0": "illusion_15L", "i_front1": "illusion_15L", "i_rear0": "illusion_15L", "i_rear1": "illusion_15L",
|
||||
}
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
@@ -65,7 +75,7 @@ def roles_for(root: Path, pipeline: str, group: str):
|
||||
|
||||
|
||||
def field_spec(pipeline: str, group: str, role_dir: Path):
|
||||
if pipeline == "v5" or group in LEGACY_PERIODIC or (group == "erase" and role_dir.name != "target"):
|
||||
if pipeline == "v5" or group in LEGACY_PERIODIC + SR_GENERALIZATION or (group == "erase" and role_dir.name != "target"):
|
||||
return "phase_fields.npz", "target_phase", 0.0
|
||||
if group in LEGACY_VORTEX:
|
||||
return "event_fields.npz", "relative_offsets", -10
|
||||
@@ -124,6 +134,78 @@ def padded_limits(values):
|
||||
return [lo - pad, hi + pad]
|
||||
|
||||
|
||||
def sha256_file(path: Path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def plot_role_set(stage: Path, data_root: Path, group: str, roles, output_name: str):
|
||||
loaded = [load_role(path, "legacy", group) for _, path in roles]
|
||||
shapes = {item[0].shape for item in loaded}
|
||||
if len(shapes) != 1:
|
||||
fail(f"role field shapes differ for SR/{group}: {shapes}")
|
||||
all_sensors = np.concatenate([item[1] for item in loaded], axis=0)
|
||||
xlim = padded_limits(all_sensors[:, [0, 2, 4]])
|
||||
ylim = padded_limits(all_sensors[:, [1, 3, 5]])
|
||||
ny, nx = next(iter(shapes)); extent = (0, (nx - 1) / L0, 0, (ny - 1) / L0)
|
||||
fig, axes = plt.subplots(2, len(roles), figsize=(6 * len(roles), 8), constrained_layout=True, squeeze=False)
|
||||
images = []
|
||||
for col, ((label, _), (omega, sensors, units, _, _, _)) in enumerate(zip(roles, loaded)):
|
||||
images.append(axes[0, col].imshow(omega, origin="lower", extent=extent, cmap="RdBu_r", vmin=-VORTICITY_LIMIT, vmax=VORTICITY_LIMIT, aspect="equal"))
|
||||
axes[0, col].set_title(f"LEGACY / {group} — {label}")
|
||||
axes[0, col].set_xlabel("x/L0"); axes[0, col].set_ylabel("y/L0")
|
||||
axes[0, col].set_xlim(extent[:2]); axes[0, col].set_ylim(extent[2:])
|
||||
for (u, v, sensor_label), color in zip(SENSOR_PAIRS, SENSOR_COLORS):
|
||||
axes[1, col].plot(sensors[:, u], sensors[:, v], color=color, lw=1.1, alpha=0.85, label=sensor_label)
|
||||
axes[1, col].axhline(0, color="0.35", lw=0.7); axes[1, col].axvline(0, color="0.35", lw=0.7)
|
||||
axes[1, col].grid(True, alpha=0.25); axes[1, col].set_xlim(xlim); axes[1, col].set_ylim(ylim)
|
||||
axes[1, col].set_xlabel(f"sensor u ({units})"); axes[1, col].set_ylabel(f"sensor v ({units})")
|
||||
axes[1, col].set_box_aspect(0.8); axes[1, col].legend(loc="best", frameon=False)
|
||||
cbar = fig.colorbar(images[0], ax=axes[0, :], orientation="vertical", shrink=0.92, pad=0.015)
|
||||
cbar.set_label(r"$\omega_z$ (lattice$^{-1}$)")
|
||||
output = stage / "legacy" / f"{output_name}.png"; output.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(output, dpi=160); plt.close(fig)
|
||||
role_entries = []
|
||||
for (label, path), item in zip(roles, loaded):
|
||||
sources = [path / "metadata.json", path / "timeseries.csv", item[3]]
|
||||
role_entries.append({"role": label, "source": str(path), "field_file": str(item[3]), "field_slot": 0,
|
||||
"selector": item[4], "selector_value": float(item[5]),
|
||||
"source_files": [{"path": str(source), "sha256": sha256_file(source), "bytes": source.stat().st_size} for source in sources]})
|
||||
return {"pipeline": "legacy-sr", "group": group, "plot": f"legacy/{output_name}.png", "roles": role_entries,
|
||||
"vorticity_limit": VORTICITY_LIMIT, "sensor_limits": {"u": xlim, "v": ylim}}
|
||||
|
||||
|
||||
def sr_plot_specs(data_root: Path):
|
||||
legacy = data_root / "legacy"; specs = []
|
||||
for case in SR_TRAINING:
|
||||
base = legacy / case
|
||||
specs.append((case, (("Target", base / "target"), ("PPO", base / "controlled"), ("SR", base / "sr"), ("Zero (physical, no control)", base / "zero")), case))
|
||||
for case in SR_GENERALIZATION:
|
||||
base = legacy / case
|
||||
specs.append((case, (("Target", base / "target"), ("SR", base / "sr"), ("Zero (physical, no control)", base / "zero")), case))
|
||||
for variant, case in SR_VARIANT_CASE.items():
|
||||
base = legacy / case
|
||||
specs.append((case, (("Target", base / "target"), ("Parent SR", base / "sr"), (f"Variant SR ({variant})", base / f"sr_{variant}"), ("Zero (physical, no control)", base / "zero")), f"variants/{case}_{variant}"))
|
||||
return specs
|
||||
|
||||
|
||||
def render_sr_package(stage: Path, data_root: Path, allow_partial=False):
|
||||
entries = []
|
||||
for group, roles, output_name in sr_plot_specs(data_root):
|
||||
missing = [str(path) for _, path in roles if not path.is_dir()]
|
||||
if missing:
|
||||
if allow_partial:
|
||||
continue
|
||||
fail(f"missing SR plot roles for {group}: {missing}")
|
||||
entries.append(plot_role_set(stage, data_root, group, roles, output_name))
|
||||
if not allow_partial and len(entries) != 23:
|
||||
fail(f"expected 23 SR plots, got {len(entries)}")
|
||||
return entries
|
||||
|
||||
|
||||
def plot_group(stage: Path, data_root: Path, pipeline: str, group: str):
|
||||
roles = roles_for(data_root, pipeline, group)
|
||||
loaded = [load_role(path, pipeline, group) for _, path in roles]
|
||||
@@ -171,27 +253,38 @@ def main() -> None:
|
||||
here = Path(__file__).resolve().parent
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--data-root", type=Path, default=here / "data" / "reproduction")
|
||||
parser.add_argument("--output-root", type=Path, default=here / "data" / "reproduction_plots")
|
||||
parser.add_argument("--output-root", type=Path)
|
||||
parser.add_argument("--campaign", choices=("ppo", "sr"), default="ppo")
|
||||
parser.add_argument("--allow-partial", action="store_true")
|
||||
args = parser.parse_args()
|
||||
data_root, output_root = args.data_root.resolve(), args.output_root.absolute()
|
||||
require_exact_groups(data_root)
|
||||
data_root = args.data_root.resolve()
|
||||
output_root = (args.output_root or here / "data" / ("reproduction_plots_sr" if args.campaign == "sr" else "reproduction_plots")).absolute()
|
||||
if args.campaign == "ppo": require_exact_groups(data_root)
|
||||
if output_root.is_symlink():
|
||||
fail(f"refusing symlink output root: {output_root}")
|
||||
output_root.parent.mkdir(parents=True, exist_ok=True)
|
||||
stage = Path(tempfile.mkdtemp(prefix=f".{output_root.name}.staging-", dir=output_root.parent))
|
||||
backup = output_root.with_name(f".{output_root.name}.old-{os.getpid()}")
|
||||
try:
|
||||
entries = [plot_group(stage, data_root, "v5", group) for group in sorted(V5_GROUPS)]
|
||||
entries += [plot_group(stage, data_root, "legacy", group) for group in LEGACY_GROUPS]
|
||||
if len(entries) != 30:
|
||||
fail(f"expected 30 plots, got {len(entries)}")
|
||||
(stage / "manifest.json").write_text(json.dumps({
|
||||
"schema": "drl-pinball-reproduction-plots-v1",
|
||||
"plot_count": 30,
|
||||
"vorticity_limit": VORTICITY_LIMIT,
|
||||
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel",
|
||||
"plots": entries,
|
||||
}, indent=2) + "\n")
|
||||
if args.campaign == "sr":
|
||||
entries = render_sr_package(stage, data_root, allow_partial=args.allow_partial)
|
||||
manifest = {"schema": "drl-pinball-sr-reproduction-plots-v1", "plot_count": len(entries),
|
||||
"expected_plot_count": 23, "allow_partial": args.allow_partial,
|
||||
"no_ppo_cases": list(SR_GENERALIZATION),
|
||||
"no_ppo_contract": "Generalization conditions intentionally contain Target/SR/Zero only; PPO is absent by acquisition design.",
|
||||
"variant_contract": "Each diagnostic binds the named variant to its same-case parent SR.",
|
||||
"vorticity_limit": VORTICITY_LIMIT,
|
||||
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel",
|
||||
"plots": entries}
|
||||
else:
|
||||
entries = [plot_group(stage, data_root, "v5", group) for group in sorted(V5_GROUPS)]
|
||||
entries += [plot_group(stage, data_root, "legacy", group) for group in LEGACY_GROUPS]
|
||||
if len(entries) != 30:
|
||||
fail(f"expected 30 plots, got {len(entries)}")
|
||||
manifest = {"schema": "drl-pinball-reproduction-plots-v1", "plot_count": 30,
|
||||
"vorticity_limit": VORTICITY_LIMIT,
|
||||
"vorticity_contract": "fixed symmetric [-0.001, +0.001] for every vorticity panel", "plots": entries}
|
||||
(stage / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
if output_root.exists():
|
||||
os.replace(output_root, backup)
|
||||
os.replace(stage, output_root)
|
||||
@@ -202,7 +295,7 @@ def main() -> None:
|
||||
if backup.exists() and not output_root.exists():
|
||||
os.replace(backup, output_root)
|
||||
raise
|
||||
print(f"wrote 30 plots and manifest to {output_root}")
|
||||
print(f"wrote {len(entries)} plots and manifest to {output_root}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user