Rewrite analysis as streaming rollout-vs-reference plotting pipeline
Replace the monolithic giant/analysis.py (predict-local + RolloutVsTruth
diagnostics) with a lean giant/analysis/ package that compares one
autoregressive `giant rollout` for a checkpoint against a held-out
miniCaloSim reference file, and generates publication-styled plots in
parallel on HTCondor.
Rollout output and a raw reference file share a world-frame physical
column subset under identical names, so the old ALR/local-frame decode
machinery is gone — everything is world-frame mm/MeV.
- sources.py: canonical LazyFrames, synthetic-termination-row filtering,
the secondary view (rollout generation>0 tracks vs reference sec_*_list).
- reduce.py: streaming primitives — a single hist1d group_by pass, per-event
scalars, edep-weighted depth/transverse profiles, species share, leakage.
- context.py/grouping.py: prep resolves fixed bin edges + energy/pdg/material
group sets once into shared.json, so each compute job is one pass, no range
scan (histogram efficiency).
- catalog.py: declarative PlotSpec registry — marginals x {overall,energy,pdg,
material}, per-event totals, shower profiles, species/leakage, secondaries.
- render.py: the only plotstyle/LaTeX importer; PDFs + gallery metadata.
- condor.py + `giant analyze` CLI (prep/compute-one/list/render/submit):
one job per plot, compute/render split (workers polars-only, no LaTeX).
Styling via ETPlot's plotstyle (added to the analysis extra). New tests cover
the reduce primitives, catalog id uniqueness + compute, condor submit, and a
guarded render smoke test. Delete the two predict-diagnostics notebooks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""Render reduced artifacts to styled PDFs + gallery metadata (the local step).
|
||||
|
||||
This is the *only* module that imports ``plotstyle`` (ETPlot's KIT matplotlib
|
||||
theme), which renders through a real LaTeX toolchain — so it runs on the
|
||||
submit/login node, never on a compute worker. It reads nothing but the small
|
||||
``Reduced`` JSON files a run produced, so it is fully decoupled from the heavy
|
||||
streaming compute.
|
||||
|
||||
For each reduced artifact it writes ``<out>/<family>/<id>.pdf`` plus a sibling
|
||||
``<id>.yaml`` (per-plot gallery metadata) and a per-family ``metadata.yaml``.
|
||||
Optionally runs ``gallery generate`` to build the static HTML site.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import plotstyle as ps # ty: ignore[unresolved-import]
|
||||
import yaml
|
||||
|
||||
from giant.analysis.reduced import Reduced
|
||||
|
||||
_SERIES_LABELS = {"rollout": "rollout", "reference": "reference (Geant4)"}
|
||||
|
||||
|
||||
def _density(counts: list[int] | np.ndarray, edges: np.ndarray) -> np.ndarray:
|
||||
counts = np.asarray(counts, dtype=np.float64)
|
||||
total = counts.sum()
|
||||
if total == 0:
|
||||
return counts
|
||||
return counts / (total * (edges[1] - edges[0]))
|
||||
|
||||
|
||||
def _overlay(ax, edges: np.ndarray, series: dict[str, list], log_y: bool) -> None:
|
||||
for key in ("reference", "rollout"):
|
||||
if key in series:
|
||||
ax.stairs(_density(series[key], edges), edges, label=_SERIES_LABELS[key])
|
||||
if log_y:
|
||||
ax.set_yscale("log")
|
||||
|
||||
|
||||
def _render_overlay(r: Reduced):
|
||||
edges = np.asarray(r.payload["edges"])
|
||||
fig, ax = ps.new_figure("thesis-single", title=r.title)
|
||||
_overlay(ax, edges, r.payload, r.payload.get("log_y", False))
|
||||
ax.set_xlabel(r.xlabel)
|
||||
ax.set_ylabel("density")
|
||||
ps.style_legend(ax, title="source")
|
||||
return fig
|
||||
|
||||
|
||||
def _render_single(r: Reduced):
|
||||
edges = np.asarray(r.payload["edges"])
|
||||
fig, ax = ps.new_figure("thesis-single", title=r.title)
|
||||
ax.stairs(
|
||||
_density(r.payload["rollout"], edges), edges, label=_SERIES_LABELS["rollout"]
|
||||
)
|
||||
if r.payload.get("log_y"):
|
||||
ax.set_yscale("log")
|
||||
ax.set_xlabel(r.xlabel)
|
||||
ax.set_ylabel("density")
|
||||
ps.style_legend(ax, title="source")
|
||||
return fig
|
||||
|
||||
|
||||
def _render_grouped(r: Reduced):
|
||||
edges = np.asarray(r.payload["edges"])
|
||||
groups = r.payload["groups"]
|
||||
labels = list(groups)
|
||||
n = len(labels)
|
||||
ncols = min(3, n) or 1
|
||||
nrows = (n + ncols - 1) // ncols
|
||||
fig, axes = ps.new_figure(
|
||||
"slide-16x9", title=r.title, nrows=nrows, ncols=ncols, squeeze=False
|
||||
)
|
||||
flat = axes.ravel()
|
||||
for i, lbl in enumerate(labels):
|
||||
ax = flat[i]
|
||||
_overlay(ax, edges, groups[lbl], r.payload.get("log_y", False))
|
||||
ax.set_title(lbl, fontsize=8)
|
||||
ax.set_xlabel(r.xlabel)
|
||||
for j in range(n, len(flat)):
|
||||
flat[j].set_visible(False)
|
||||
ps.style_legend(flat[0], title="source")
|
||||
return fig
|
||||
|
||||
|
||||
def _render_profile(r: Reduced):
|
||||
edges = np.asarray(r.payload["edges"])
|
||||
centers = 0.5 * (edges[:-1] + edges[1:])
|
||||
fig, ax = ps.new_figure("thesis-single", title=r.title)
|
||||
for key in ("reference", "rollout"):
|
||||
mean = np.asarray(r.payload[f"{key}_mean"])
|
||||
std = np.asarray(r.payload[f"{key}_std"])
|
||||
(line,) = ax.plot(centers, mean, label=_SERIES_LABELS[key])
|
||||
ax.fill_between(
|
||||
centers, mean - std, mean + std, alpha=0.2, color=line.get_color()
|
||||
)
|
||||
ax.set_xlabel(r.xlabel)
|
||||
ax.set_ylabel(r.payload.get("ylabel", "mean deposited energy [MeV]"))
|
||||
ps.style_legend(ax, title="source")
|
||||
return fig
|
||||
|
||||
|
||||
def _render_bar(r: Reduced):
|
||||
labels = r.payload["labels"]
|
||||
x = np.arange(len(labels))
|
||||
width = 0.4
|
||||
fig, ax = ps.new_figure("thesis-single", title=r.title)
|
||||
ax.bar(
|
||||
x - width / 2, r.payload["reference"], width, label=_SERIES_LABELS["reference"]
|
||||
)
|
||||
ax.bar(x + width / 2, r.payload["rollout"], width, label=_SERIES_LABELS["rollout"])
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(labels, rotation=45, ha="right")
|
||||
ax.set_ylabel(r.payload.get("ylabel", "value"))
|
||||
ps.style_legend(ax, title="source")
|
||||
return fig
|
||||
|
||||
|
||||
_RENDERERS = {
|
||||
"overlay_hist": _render_overlay,
|
||||
"single_hist": _render_single,
|
||||
"grouped_hist": _render_grouped,
|
||||
"profile": _render_profile,
|
||||
"bar": _render_bar,
|
||||
}
|
||||
|
||||
|
||||
def render(r: Reduced):
|
||||
"""Build the matplotlib figure for one reduced artifact (dispatch on kind)."""
|
||||
return _RENDERERS[r.kind](r)
|
||||
|
||||
|
||||
def _plot_metadata(r: Reduced, run_meta: dict) -> dict:
|
||||
meta = {
|
||||
"title": r.title,
|
||||
"description": f"Rollout vs reference: {r.title}.",
|
||||
"plot_type": r.kind,
|
||||
"family": r.family,
|
||||
}
|
||||
meta.update(r.meta)
|
||||
if "note" in r.payload:
|
||||
meta["note"] = r.payload["note"]
|
||||
return meta
|
||||
|
||||
|
||||
def render_all(
|
||||
reduced_dir: str | Path,
|
||||
out_dir: str | Path,
|
||||
run_meta: dict | None = None,
|
||||
*,
|
||||
run_gallery: bool = False,
|
||||
) -> list[Path]:
|
||||
"""Render every reduced artifact under ``reduced_dir`` to a PDF tree.
|
||||
|
||||
Writes ``<out>/<family>/<id>.pdf`` + ``<id>.yaml`` and a per-family
|
||||
``metadata.yaml`` (carrying the run's checkpoint/paths as gallery params).
|
||||
Returns the list of PDF paths written.
|
||||
"""
|
||||
ps.use()
|
||||
run_meta = run_meta or {}
|
||||
reduced_dir, out_dir = Path(reduced_dir), Path(out_dir)
|
||||
pdfs: list[Path] = []
|
||||
families: set[str] = set()
|
||||
|
||||
for jf in sorted(reduced_dir.glob("*.json")):
|
||||
r = Reduced.load(jf)
|
||||
family_dir = out_dir / r.family
|
||||
family_dir.mkdir(parents=True, exist_ok=True)
|
||||
families.add(r.family)
|
||||
fig = render(r)
|
||||
ps.savefig(fig, str(family_dir / r.id), formats=("pdf",))
|
||||
(family_dir / f"{r.id}.yaml").write_text(
|
||||
yaml.safe_dump(_plot_metadata(r, run_meta), sort_keys=False)
|
||||
)
|
||||
pdfs.append(family_dir / f"{r.id}.pdf")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.close(fig)
|
||||
|
||||
# Root + per-family gallery metadata.
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / "metadata.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"title": run_meta.get("title", "GIANT rollout analysis"),
|
||||
"description": "Autoregressive rollout compared against held-out Geant4 reference steps.",
|
||||
"experiment": "GIANT",
|
||||
"parameters": run_meta,
|
||||
},
|
||||
sort_keys=False,
|
||||
)
|
||||
)
|
||||
for fam in families:
|
||||
(out_dir / fam / "metadata.yaml").write_text(
|
||||
yaml.safe_dump(
|
||||
{"title": fam, "description": f"{fam} plots."}, sort_keys=False
|
||||
)
|
||||
)
|
||||
|
||||
if run_gallery:
|
||||
subprocess.run(["gallery", "generate", "--source", str(out_dir)], check=True)
|
||||
return pdfs
|
||||
Reference in New Issue
Block a user