analyze: drive prep/submit from the rollout YAML sidecar
`giant analyze prep` / `submit` now take the `giant rollout` YAML sidecar as their only positional input instead of explicit --rollout/--reference/--out-dir. The YAML's `output`/`dataset` keys name the rollout parquet and its seed file (the reference truth), and the rest of the sidecar (checkpoint, geometry oracle, cutoffs) flows into every plot's gallery metadata. prep derives its own run directory next to the rollout parquet (<...>/analysis_<id>/) holding shared.json, run_meta.json, reduced/, plots/. compute-one and render now take just --run-dir / a run-dir argument and read the resolved paths + metadata from run_meta.json, so the condor wrapper no longer threads file paths. open_side scans a directory of reference shards via glob. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -14,9 +14,8 @@ giant train path/to/steps.parquet --mode flow # train (flow matching)
|
||||
giant train path/to/steps.parquet --mode ddpm # train (DDPM baseline)
|
||||
giant predict path/to/steps.parquet --checkpoint ckpt/best.pt # per-step predictions
|
||||
giant rollout path/to/steps.parquet --checkpoint ckpt/best.pt --geometry oracle.pkl # full showers
|
||||
giant analyze submit --rollout roll.parquet --reference test.parquet --out-dir run/ \
|
||||
--accounting-group cms # parallel rollout-vs-reference analysis on HTCondor
|
||||
giant analyze render --reduced-dir run/reduced --out run/plots --gallery # render PDFs + HTML gallery
|
||||
giant analyze submit rollout.yaml --accounting-group cms # parallel rollout-vs-reference analysis on HTCondor
|
||||
giant analyze render <run_dir> --gallery # render PDFs + HTML gallery (run_dir from prep/submit)
|
||||
dwarf --help # dataset/tooling CLI: convert, migrate, bump-gen,
|
||||
# bump-schema, status, update-manifest, create-manifest,
|
||||
# make-root, build-geometry-oracle, hparam-scan
|
||||
@@ -59,7 +58,7 @@ GIANT is a conditional generative surrogate for the Geant4 step function. It rep
|
||||
|
||||
**Validation** (`giant/validate.py`): step-level marginal comparisons.
|
||||
|
||||
**Analysis** (`giant/analysis/`, `giant analyze` CLI): a lean, streaming rollout-vs-reference plotting pipeline that compares one autoregressive `giant rollout` (for a given checkpoint) against a held-out miniCaloSim reference steps file, and produces publication-styled PDFs assembled into an HTML gallery. It exploits the fact that rollout output and a raw reference file share a world-frame physical column subset under identical names (`pre_*`/`post_*`/`edep`/`step_length`/`pdg`/`material`/`event_id`), so no ALR/local-frame decode is needed — everything is world-frame mm/MeV. Structure: `sources.py` (canonical LazyFrames + synthetic-termination-row filtering + the secondary view, which is `generation>0 & step_no==0` rollout tracks vs exploded `sec_*_list` reference columns), `reduce.py` (the streaming primitives — a single `hist1d` `group_by([group,bin]).len()` pass, per-event scalars, edep-weighted depth/transverse profiles, species share, leakage), `grouping.py`/`context.py` (fixed bin edges + energy-quantile/pdg/material group sets resolved once by `prep` into `shared.json`, so every compute job is one pass with no range scan), `catalog.py` (the declarative `PlotSpec` registry — marginals × {overall,energy,pdg,material}, per-event totals, shower profiles, species/leakage, secondaries), and `render.py` (the only module importing ETPlot's `plotstyle`/LaTeX; dispatches on `Reduced.kind`, writes PDFs + `metadata.yaml`). **Compute/render split:** `giant analyze submit` runs `prep` then submits one HTCondor job per plot (`compute-one`, polars/numpy only — no LaTeX on workers), each writing a small `reduced/<id>.json`; the local `giant analyze render` turns those into the styled PDF/gallery tree. See `giant/analysis/__init__.py`.
|
||||
**Analysis** (`giant/analysis/`, `giant analyze` CLI): a lean, streaming rollout-vs-reference plotting pipeline that compares one autoregressive `giant rollout` (for a given checkpoint) against a held-out miniCaloSim reference steps file, and produces publication-styled PDFs assembled into an HTML gallery. It exploits the fact that rollout output and a raw reference file share a world-frame physical column subset under identical names (`pre_*`/`post_*`/`edep`/`step_length`/`pdg`/`material`/`event_id`), so no ALR/local-frame decode is needed — everything is world-frame mm/MeV. Structure: `sources.py` (canonical LazyFrames + synthetic-termination-row filtering + the secondary view, which is `generation>0 & step_no==0` rollout tracks vs exploded `sec_*_list` reference columns), `reduce.py` (the streaming primitives — a single `hist1d` `group_by([group,bin]).len()` pass, per-event scalars, edep-weighted depth/transverse profiles, species share, leakage), `grouping.py`/`context.py` (fixed bin edges + energy-quantile/pdg/material group sets resolved once by `prep` into `shared.json`, so every compute job is one pass with no range scan), `catalog.py` (the declarative `PlotSpec` registry — marginals × {overall,energy,pdg,material}, per-event totals, shower profiles, species/leakage, secondaries), and `render.py` (the only module importing ETPlot's `plotstyle`/LaTeX; dispatches on `Reduced.kind`, writes PDFs + `metadata.yaml`). **Input is a `giant rollout` YAML sidecar** (`condor.py:load_rollout_yaml`): its `output`/`dataset` keys name the rollout parquet and the seed file (= the reference truth), and the rest of the YAML (checkpoint, geometry oracle, cutoffs) flows into each plot's gallery metadata. `prep` derives its own **run directory** next to the rollout parquet (`<...>/analysis_<id>/`) holding `shared.json`, `run_meta.json`, `reduced/`, `plots/`. **Compute/render split:** `giant analyze submit rollout.yaml` runs `prep` then submits one HTCondor job per plot (`compute-one --run-dir`, polars/numpy only — no LaTeX on workers), each writing a small `reduced/<id>.json`; the local `giant analyze render <run_dir>` turns those into the styled PDF/gallery tree. See `giant/analysis/__init__.py`.
|
||||
|
||||
**Shower rollout** (`giant/rollout.py`, `giant rollout` CLI): autoregressively steps the two-stage model into a full shower — each primary post-step becomes the next pre-step, secondaries are pushed as new tracks, and per-step `material`/`layer_id` come from a `GeometryOracle` (`giant/geometry.py`, built via `dwarf build-geometry-oracle`) that learns position → (material, layer_id) from data and flags detector escape by nearest-neighbour distance. Tracks terminate on energy cutoff, per-track max steps, escape, or natural end; energy is deposited locally on every stop except escape (leakage), so showers conserve energy by construction.
|
||||
|
||||
|
||||
@@ -10,7 +10,16 @@ re-exported here is plotstyle-free so it runs on a compute worker. Import
|
||||
"""
|
||||
|
||||
from giant.analysis.catalog import build_catalog, catalog_ids, get_spec
|
||||
from giant.analysis.condor import SubmitConfig, compute_one, prep, write_submit
|
||||
from giant.analysis.condor import (
|
||||
RunMeta,
|
||||
SubmitConfig,
|
||||
compute_one,
|
||||
compute_reduced,
|
||||
derive_run_dir,
|
||||
load_rollout_yaml,
|
||||
prep,
|
||||
write_submit,
|
||||
)
|
||||
from giant.analysis.context import Context, build_context
|
||||
from giant.analysis.reduced import Reduced
|
||||
from giant.analysis.sources import Side
|
||||
@@ -19,8 +28,12 @@ __all__ = [
|
||||
"build_catalog",
|
||||
"catalog_ids",
|
||||
"get_spec",
|
||||
"RunMeta",
|
||||
"SubmitConfig",
|
||||
"compute_one",
|
||||
"compute_reduced",
|
||||
"derive_run_dir",
|
||||
"load_rollout_yaml",
|
||||
"prep",
|
||||
"write_submit",
|
||||
"Context",
|
||||
|
||||
+153
-57
@@ -1,15 +1,32 @@
|
||||
"""HTCondor orchestration: prep, per-plot compute, and the submit description.
|
||||
"""HTCondor orchestration driven by a ``giant rollout`` YAML sidecar.
|
||||
|
||||
A rollout writes a YAML sidecar (``giant/cli.py:_write_prediction_ref`` +
|
||||
rollout extras) that already names both files we need and carries the run's
|
||||
provenance:
|
||||
|
||||
* ``output`` — the rollout steps parquet (the *generated* side)
|
||||
* ``dataset`` — the file the rollout was seeded from, i.e. the held-out real
|
||||
steps (the *reference* side)
|
||||
* ``checkpoint``, ``geometry_oracle``, ``energy_cutoff``, ``steps``, ... —
|
||||
metadata that flows straight into every plot's gallery ``metadata.yaml``.
|
||||
|
||||
So the analysis takes that one YAML as input, derives its own **run directory**
|
||||
next to the rollout parquet, and lays everything out under it:
|
||||
|
||||
<run_dir>/shared.json fixed bin edges / group sets (prep)
|
||||
<run_dir>/run_meta.json resolved rollout/reference paths + plot metadata
|
||||
<run_dir>/reduced/<id>.json one per compute job
|
||||
<run_dir>/plots/<family>/<id>.pdf rendered locally
|
||||
|
||||
Job model (one condor job per plot, compute/render split):
|
||||
|
||||
1. ``prep`` runs once on the submit node — resolves the shared context (fixed bin
|
||||
edges, energy quantiles, top species/materials) from a subsample and writes
|
||||
``shared.json``. Cheap; no LaTeX.
|
||||
2. one job per catalog id runs ``giant analyze compute-one`` on a worker — a
|
||||
single streaming pass producing ``reduced/<id>.json``. polars/numpy only, no
|
||||
LaTeX, so it needs no plotstyle in the container.
|
||||
3. a final *local* ``giant analyze render`` turns every reduced artifact into a
|
||||
styled PDF + gallery metadata (that step imports plotstyle/LaTeX).
|
||||
1. ``prep`` runs once on the submit node — reads the YAML, resolves the shared
|
||||
context from a subsample, writes ``shared.json`` + ``run_meta.json``.
|
||||
2. one job per catalog id runs ``giant analyze compute-one --run-dir`` on a
|
||||
worker — a single streaming pass writing ``reduced/<id>.json`` (polars/numpy
|
||||
only, no LaTeX).
|
||||
3. a final *local* ``giant analyze render`` turns those into the styled PDF +
|
||||
gallery tree (that step imports plotstyle/LaTeX).
|
||||
|
||||
Files on ``/ceph`` or ``/work`` are reached via ``ProvidesETPResources``; no
|
||||
HTCondor file transfer of the multi-GB inputs.
|
||||
@@ -17,26 +34,123 @@ HTCondor file transfer of the multi-GB inputs.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from giant.analysis.catalog import Bundle, catalog_ids, get_spec
|
||||
from giant.analysis.context import Context, build_context
|
||||
|
||||
# Keys copied verbatim from a rollout YAML into each plot's gallery metadata.
|
||||
_PLOT_META_KEYS = (
|
||||
"prediction_id",
|
||||
"checkpoint",
|
||||
"output",
|
||||
"dataset",
|
||||
"geometry_oracle",
|
||||
"energy_cutoff",
|
||||
"max_steps",
|
||||
"steps",
|
||||
"max_tracks_per_event",
|
||||
"n_seed_events",
|
||||
"timestamp",
|
||||
"comment",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# rollout-YAML → run directory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_rollout_yaml(path: str | Path) -> dict:
|
||||
"""Load a ``giant rollout`` YAML sidecar, requiring the two file paths."""
|
||||
d = yaml.safe_load(Path(path).read_text())
|
||||
for key in ("output", "dataset"):
|
||||
if key not in d:
|
||||
raise ValueError(
|
||||
f"{path} is not a rollout YAML (missing {key!r}); expected the "
|
||||
"sidecar `giant rollout` writes next to the checkpoint"
|
||||
)
|
||||
if d.get("kind") not in (None, "rollout"):
|
||||
raise ValueError(f"{path} has kind={d.get('kind')!r}, not a rollout YAML")
|
||||
return d
|
||||
|
||||
|
||||
def derive_run_dir(rollout_yaml: dict, run_dir: str | Path | None = None) -> Path:
|
||||
"""Analysis output directory, next to the rollout parquet unless overridden."""
|
||||
if run_dir is not None:
|
||||
return Path(run_dir)
|
||||
rollout = Path(rollout_yaml["output"])
|
||||
tag = str(rollout_yaml.get("prediction_id") or rollout.stem)[:8]
|
||||
return rollout.parent / f"analysis_{tag}"
|
||||
|
||||
|
||||
def _plot_meta(rollout_yaml: dict) -> dict:
|
||||
return {k: rollout_yaml[k] for k in _PLOT_META_KEYS if k in rollout_yaml}
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunMeta:
|
||||
"""Resolved paths + plot metadata for one analysis run (``run_meta.json``)."""
|
||||
|
||||
rollout: str
|
||||
reference: str
|
||||
run_dir: str
|
||||
title: str
|
||||
plot_meta: dict
|
||||
|
||||
def save(self, path: str | Path) -> None:
|
||||
Path(path).write_text(json.dumps(self.__dict__, indent=2))
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path) -> "RunMeta":
|
||||
return cls(**json.loads(Path(path).read_text()))
|
||||
|
||||
|
||||
def prep(
|
||||
rollout_yaml: str | Path,
|
||||
run_dir: str | Path | None = None,
|
||||
**ctx_kwargs,
|
||||
) -> Path:
|
||||
"""Read the rollout YAML, build the shared context, and lay out the run dir.
|
||||
|
||||
Writes ``shared.json`` + ``run_meta.json`` and returns the run directory.
|
||||
"""
|
||||
y = load_rollout_yaml(rollout_yaml)
|
||||
run_path = derive_run_dir(y, run_dir)
|
||||
run_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rollout, reference = y["output"], y["dataset"]
|
||||
ctx = build_context(rollout, reference, **ctx_kwargs)
|
||||
ctx.save(run_path / "shared.json")
|
||||
|
||||
ckpt = Path(y.get("checkpoint", "")).name or "rollout"
|
||||
RunMeta(
|
||||
rollout=str(rollout),
|
||||
reference=str(reference),
|
||||
run_dir=str(run_path),
|
||||
title=f"GIANT rollout analysis — {ckpt}",
|
||||
plot_meta=_plot_meta(y),
|
||||
).save(run_path / "run_meta.json")
|
||||
return run_path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# per-plot compute (what each condor job runs)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def compute_one(
|
||||
def compute_reduced(
|
||||
spec_id: str,
|
||||
rollout: str | Path,
|
||||
reference: str | Path,
|
||||
shared: str | Path,
|
||||
out: str | Path,
|
||||
) -> Path:
|
||||
"""Run one plot's streaming reduction and write its ``Reduced`` JSON."""
|
||||
"""Core: run one plot's reduction against explicit paths → ``Reduced`` JSON."""
|
||||
ctx = Context.load(shared)
|
||||
bundle = Bundle.open(rollout, reference, ctx)
|
||||
reduced = get_spec(spec_id).compute(bundle)
|
||||
@@ -45,6 +159,19 @@ def compute_one(
|
||||
return out
|
||||
|
||||
|
||||
def compute_one(spec_id: str, run_dir: str | Path) -> Path:
|
||||
"""Run one plot's reduction from a prepped run directory."""
|
||||
run_path = Path(run_dir)
|
||||
meta = RunMeta.load(run_path / "run_meta.json")
|
||||
return compute_reduced(
|
||||
spec_id,
|
||||
meta.rollout,
|
||||
meta.reference,
|
||||
run_path / "shared.json",
|
||||
run_path / "reduced" / f"{spec_id}.json",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# submit description
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -52,9 +179,7 @@ def compute_one(
|
||||
|
||||
@dataclass
|
||||
class SubmitConfig:
|
||||
rollout: Path
|
||||
reference: Path
|
||||
out_dir: Path
|
||||
run_dir: Path
|
||||
accounting_group: str
|
||||
repo_dir: Path
|
||||
docker_image: str = "mschnepf/slc7-condocker"
|
||||
@@ -67,18 +192,13 @@ class SubmitConfig:
|
||||
_WRAPPER = """#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd {repo_dir}
|
||||
exec uv run giant analyze compute-one \\
|
||||
--id "$1" \\
|
||||
--rollout {rollout} \\
|
||||
--reference {reference} \\
|
||||
--shared {shared} \\
|
||||
--out {reduced_dir}/"$1".json
|
||||
exec uv run giant analyze compute-one --id "$1" --run-dir {run_dir}
|
||||
"""
|
||||
|
||||
|
||||
def _submit_description(cfg: SubmitConfig, wrapper: Path, ids_file: Path) -> str:
|
||||
reqs_attrs = (
|
||||
"+RemoteJob = True\nrequest_walltime = {wt}\n".format(wt=cfg.request_walltime_s)
|
||||
"+RemoteJob = True\n"
|
||||
if cfg.remote
|
||||
else "requirements = TARGET.ProvidesETPResources\n"
|
||||
)
|
||||
@@ -94,9 +214,9 @@ def _submit_description(cfg: SubmitConfig, wrapper: Path, ids_file: Path) -> str
|
||||
f"+RequestWalltime = {cfg.request_walltime_s}\n"
|
||||
f"accounting_group = {cfg.accounting_group}\n"
|
||||
f"{reqs_attrs}"
|
||||
"output = logs/$(plotid).out\n"
|
||||
"error = logs/$(plotid).err\n"
|
||||
"log = logs/condor.log\n"
|
||||
f"output = {cfg.run_dir}/logs/$(plotid).out\n"
|
||||
f"error = {cfg.run_dir}/logs/$(plotid).err\n"
|
||||
f"log = {cfg.run_dir}/logs/condor.log\n"
|
||||
f"queue plotid from {ids_file}\n"
|
||||
)
|
||||
|
||||
@@ -104,45 +224,21 @@ def _submit_description(cfg: SubmitConfig, wrapper: Path, ids_file: Path) -> str
|
||||
def write_submit(cfg: SubmitConfig, ids: list[str] | None = None) -> Path:
|
||||
"""Write the wrapper script, plot-id list, and HTCondor submit description.
|
||||
|
||||
Returns the path to the submit description (``<out_dir>/analyze.sub``). Does
|
||||
not submit — call ``condor_submit`` on the returned file, or use ``submit``.
|
||||
Returns the submit description path (``<run_dir>/analyze.sub``). Does not
|
||||
submit — call ``condor_submit`` on the returned file.
|
||||
"""
|
||||
ids = ids or catalog_ids()
|
||||
out_dir = cfg.out_dir
|
||||
reduced_dir = out_dir / "reduced"
|
||||
(out_dir / "logs").mkdir(parents=True, exist_ok=True)
|
||||
reduced_dir.mkdir(parents=True, exist_ok=True)
|
||||
run_dir = cfg.run_dir
|
||||
(run_dir / "logs").mkdir(parents=True, exist_ok=True)
|
||||
(run_dir / "reduced").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
wrapper = out_dir / "run_compute.sh"
|
||||
wrapper.write_text(
|
||||
_WRAPPER.format(
|
||||
repo_dir=cfg.repo_dir,
|
||||
rollout=cfg.rollout,
|
||||
reference=cfg.reference,
|
||||
shared=out_dir / "shared.json",
|
||||
reduced_dir=reduced_dir,
|
||||
)
|
||||
)
|
||||
wrapper = run_dir / "run_compute.sh"
|
||||
wrapper.write_text(_WRAPPER.format(repo_dir=cfg.repo_dir, run_dir=run_dir))
|
||||
wrapper.chmod(0o755)
|
||||
|
||||
ids_file = out_dir / "plotids.txt"
|
||||
ids_file = run_dir / "plotids.txt"
|
||||
ids_file.write_text("\n".join(ids) + "\n")
|
||||
|
||||
sub = out_dir / "analyze.sub"
|
||||
sub = run_dir / "analyze.sub"
|
||||
sub.write_text(_submit_description(cfg, wrapper, ids_file))
|
||||
return sub
|
||||
|
||||
|
||||
def prep(
|
||||
rollout: str | Path,
|
||||
reference: str | Path,
|
||||
out_dir: str | Path,
|
||||
**kwargs,
|
||||
) -> Path:
|
||||
"""Build and save the shared context (``<out_dir>/shared.json``)."""
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
ctx = build_context(rollout, reference, **kwargs)
|
||||
shared = out_dir / "shared.json"
|
||||
ctx.save(shared)
|
||||
return shared
|
||||
|
||||
@@ -189,7 +189,7 @@ def render_all(
|
||||
"title": run_meta.get("title", "GIANT rollout analysis"),
|
||||
"description": "Autoregressive rollout compared against held-out Geant4 reference steps.",
|
||||
"experiment": "GIANT",
|
||||
"parameters": run_meta,
|
||||
"parameters": {k: v for k, v in run_meta.items() if k != "title"},
|
||||
},
|
||||
sort_keys=False,
|
||||
)
|
||||
@@ -204,3 +204,24 @@ def render_all(
|
||||
if run_gallery:
|
||||
subprocess.run(["gallery", "generate", "--source", str(out_dir)], check=True)
|
||||
return pdfs
|
||||
|
||||
|
||||
def render_run(run_dir: str | Path, *, run_gallery: bool = False) -> list[Path]:
|
||||
"""Render a prepped run directory: ``<run_dir>/reduced`` → ``<run_dir>/plots``.
|
||||
|
||||
Pulls the rollout provenance (checkpoint, paths, cutoffs) from
|
||||
``run_meta.json`` into every plot's gallery metadata.
|
||||
"""
|
||||
from giant.analysis.condor import RunMeta
|
||||
|
||||
run_dir = Path(run_dir)
|
||||
meta = RunMeta.load(run_dir / "run_meta.json")
|
||||
run_meta = {
|
||||
"title": meta.title,
|
||||
"rollout": meta.rollout,
|
||||
"reference": meta.reference,
|
||||
**meta.plot_meta,
|
||||
}
|
||||
return render_all(
|
||||
run_dir / "reduced", run_dir / "plots", run_meta, run_gallery=run_gallery
|
||||
)
|
||||
|
||||
@@ -112,6 +112,11 @@ def open_side(source: str | Path | pl.LazyFrame, side: Side) -> pl.LazyFrame:
|
||||
path = Path(source)
|
||||
if side is Side.rollout:
|
||||
_check_rollout_metadata(path)
|
||||
return pl.scan_parquet(path)
|
||||
# The reference (a rollout's seed `dataset`) may be a directory of parquet
|
||||
# shards rather than a single file — scan them all.
|
||||
if path.is_dir():
|
||||
return pl.scan_parquet(str(path / "**/*.parquet"))
|
||||
return pl.scan_parquet(path)
|
||||
|
||||
|
||||
|
||||
+32
-32
@@ -1123,32 +1123,35 @@ app.add_typer(analyze_app, name="analyze")
|
||||
|
||||
@analyze_app.command("prep")
|
||||
def analyze_prep(
|
||||
rollout: Annotated[Path, typer.Option("--rollout", help="giant rollout parquet")],
|
||||
reference: Annotated[
|
||||
Path, typer.Option("--reference", help="Reference miniCaloSim steps parquet")
|
||||
],
|
||||
out_dir: Annotated[
|
||||
rollout_yaml: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--out-dir", "-o", help="Run directory for shared.json / reduced / plots"
|
||||
typer.Argument(
|
||||
help="giant rollout YAML sidecar (names the rollout + reference files)"
|
||||
),
|
||||
],
|
||||
run_dir: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option(
|
||||
"--run-dir",
|
||||
"-o",
|
||||
help="Override the run directory (default: next to the rollout parquet)",
|
||||
),
|
||||
] = None,
|
||||
n_energy_bins: Annotated[int, typer.Option("--energy-bins")] = 4,
|
||||
n_marginal_bins: Annotated[int, typer.Option("--bins")] = 50,
|
||||
top_k_pdg: Annotated[int, typer.Option("--top-pdg")] = 6,
|
||||
) -> None:
|
||||
"""Resolve the shared context (fixed bin edges / group sets) → shared.json."""
|
||||
"""Read the rollout YAML → shared.json + run_meta.json in the run directory."""
|
||||
from giant.analysis import prep
|
||||
|
||||
shared = prep(
|
||||
rollout,
|
||||
reference,
|
||||
out_dir,
|
||||
path = prep(
|
||||
rollout_yaml,
|
||||
run_dir,
|
||||
n_energy_bins=n_energy_bins,
|
||||
n_marginal_bins=n_marginal_bins,
|
||||
top_k_pdg=top_k_pdg,
|
||||
)
|
||||
typer.echo(f"wrote {shared}")
|
||||
typer.echo(f"run directory: {path}")
|
||||
|
||||
|
||||
@analyze_app.command("compute-one")
|
||||
@@ -1156,17 +1159,14 @@ def analyze_compute_one(
|
||||
id: Annotated[
|
||||
str, typer.Option("--id", help="Catalog plot id (see `analyze list`)")
|
||||
],
|
||||
rollout: Annotated[Path, typer.Option("--rollout")],
|
||||
reference: Annotated[Path, typer.Option("--reference")],
|
||||
shared: Annotated[
|
||||
Path, typer.Option("--shared", help="shared.json from `analyze prep`")
|
||||
run_dir: Annotated[
|
||||
Path, typer.Option("--run-dir", help="Run directory from `analyze prep`")
|
||||
],
|
||||
out: Annotated[Path, typer.Option("--out", help="Output reduced JSON path")],
|
||||
) -> None:
|
||||
"""Run one plot's streaming reduction (this is what each condor job runs)."""
|
||||
from giant.analysis import compute_one
|
||||
|
||||
path = compute_one(id, rollout, reference, shared, out)
|
||||
path = compute_one(id, run_dir)
|
||||
typer.echo(f"wrote {path}")
|
||||
|
||||
|
||||
@@ -1181,10 +1181,9 @@ def analyze_list() -> None:
|
||||
|
||||
@analyze_app.command("render")
|
||||
def analyze_render(
|
||||
reduced_dir: Annotated[
|
||||
Path, typer.Option("--reduced-dir", help="Directory of reduced *.json")
|
||||
run_dir: Annotated[
|
||||
Path, typer.Argument(help="Run directory from `analyze prep` (holds reduced/)")
|
||||
],
|
||||
out: Annotated[Path, typer.Option("--out", "-o", help="Output PDF/gallery tree")],
|
||||
gallery: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
@@ -1193,18 +1192,20 @@ def analyze_render(
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Render reduced artifacts to styled PDFs + gallery metadata (local; needs LaTeX)."""
|
||||
from giant.analysis.render import render_all
|
||||
from giant.analysis.render import render_run
|
||||
|
||||
pdfs = render_all(reduced_dir, out, run_gallery=gallery)
|
||||
typer.echo(f"rendered {len(pdfs)} plots → {out}")
|
||||
pdfs = render_run(run_dir, run_gallery=gallery)
|
||||
typer.echo(f"rendered {len(pdfs)} plots → {Path(run_dir) / 'plots'}")
|
||||
|
||||
|
||||
@analyze_app.command("submit")
|
||||
def analyze_submit(
|
||||
rollout: Annotated[Path, typer.Option("--rollout")],
|
||||
reference: Annotated[Path, typer.Option("--reference")],
|
||||
out_dir: Annotated[Path, typer.Option("--out-dir", "-o")],
|
||||
rollout_yaml: Annotated[Path, typer.Argument(help="giant rollout YAML sidecar")],
|
||||
accounting_group: Annotated[str, typer.Option("--accounting-group")],
|
||||
run_dir: Annotated[
|
||||
Optional[Path],
|
||||
typer.Option("--run-dir", "-o", help="Override the run directory"),
|
||||
] = None,
|
||||
docker_image: Annotated[
|
||||
str, typer.Option("--docker-image")
|
||||
] = "mschnepf/slc7-condocker",
|
||||
@@ -1222,11 +1223,9 @@ def analyze_submit(
|
||||
|
||||
from giant.analysis import SubmitConfig, prep, write_submit
|
||||
|
||||
prep(rollout, reference, out_dir)
|
||||
path = prep(rollout_yaml, run_dir)
|
||||
cfg = SubmitConfig(
|
||||
rollout=rollout.resolve(),
|
||||
reference=reference.resolve(),
|
||||
out_dir=out_dir,
|
||||
run_dir=path,
|
||||
accounting_group=accounting_group,
|
||||
repo_dir=Path.cwd(),
|
||||
docker_image=docker_image,
|
||||
@@ -1234,6 +1233,7 @@ def analyze_submit(
|
||||
remote=remote,
|
||||
)
|
||||
sub = write_submit(cfg)
|
||||
typer.echo(f"run directory: {path}")
|
||||
typer.echo(f"wrote submit description: {sub}")
|
||||
if dry_run:
|
||||
typer.echo("dry-run: not submitting")
|
||||
|
||||
+88
-46
@@ -1,85 +1,127 @@
|
||||
"""Tests for the HTCondor submit description + the compute_one round-trip."""
|
||||
"""Tests for the rollout-YAML → run-directory flow, compute, and submit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pyarrow.parquet as pq
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from giant.analysis import (
|
||||
Context,
|
||||
RunMeta,
|
||||
SubmitConfig,
|
||||
build_context,
|
||||
catalog_ids,
|
||||
compute_one,
|
||||
compute_reduced,
|
||||
derive_run_dir,
|
||||
load_rollout_yaml,
|
||||
prep,
|
||||
write_submit,
|
||||
)
|
||||
from giant.analysis.condor import Context
|
||||
from giant.analysis.reduced import Reduced
|
||||
from giant.constants import PREDICT_COORD_METADATA_KEY, ROLLOUT_COORD_VALUE
|
||||
from tests.test_analysis_reduce import _reference_frame, _rollout_frame
|
||||
|
||||
|
||||
def test_prep_and_compute_one_roundtrip(tmp_path: Path):
|
||||
r, t = _rollout_frame(), _reference_frame()
|
||||
ctx = build_context(
|
||||
r, t, n_energy_bins=2, n_marginal_bins=8, top_k_pdg=3, sample_rows=1000
|
||||
)
|
||||
shared = tmp_path / "shared.json"
|
||||
ctx.save(shared)
|
||||
assert Context.load(shared).top_pdgs == ctx.top_pdgs
|
||||
def _write_inputs(tmp_path: Path) -> Path:
|
||||
"""Materialize rollout+reference parquet and a rollout YAML; return the YAML path."""
|
||||
rollout = tmp_path / "rollout.parquet"
|
||||
reference = tmp_path / "reference.parquet"
|
||||
tbl = _rollout_frame().collect().to_arrow()
|
||||
tbl = tbl.replace_schema_metadata({PREDICT_COORD_METADATA_KEY: ROLLOUT_COORD_VALUE})
|
||||
pq.write_table(tbl, rollout)
|
||||
_reference_frame().collect().write_parquet(reference)
|
||||
|
||||
out = compute_one("marginal_edep", r, t, shared, tmp_path / "marginal_edep.json")
|
||||
yaml_path = tmp_path / "run.yaml"
|
||||
yaml_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"prediction_id": "abcd1234ef",
|
||||
"output": str(rollout),
|
||||
"dataset": str(reference),
|
||||
"checkpoint": "/ckpt/best.pt",
|
||||
"kind": "rollout",
|
||||
"energy_cutoff": 0.1,
|
||||
"steps": 10,
|
||||
}
|
||||
)
|
||||
)
|
||||
return yaml_path
|
||||
|
||||
|
||||
_CTX = dict(n_energy_bins=2, n_marginal_bins=8, top_k_pdg=3, sample_rows=1000)
|
||||
|
||||
|
||||
def test_load_rollout_yaml_requires_paths(tmp_path: Path):
|
||||
bad = tmp_path / "bad.yaml"
|
||||
bad.write_text(yaml.safe_dump({"output": "x.parquet"})) # no dataset
|
||||
with pytest.raises(ValueError):
|
||||
load_rollout_yaml(bad)
|
||||
|
||||
|
||||
def test_derive_run_dir_next_to_rollout():
|
||||
y = {"output": "/data/roll.parquet", "prediction_id": "abcd1234ef", "dataset": "d"}
|
||||
assert derive_run_dir(y) == Path("/data/analysis_abcd1234")
|
||||
assert derive_run_dir(y, "/somewhere") == Path("/somewhere")
|
||||
|
||||
|
||||
def test_prep_lays_out_run_dir(tmp_path: Path):
|
||||
yaml_path = _write_inputs(tmp_path)
|
||||
run_dir = prep(yaml_path, **_CTX)
|
||||
assert run_dir == tmp_path / "analysis_abcd1234"
|
||||
assert (run_dir / "shared.json").exists()
|
||||
ctx = Context.load(run_dir / "shared.json")
|
||||
assert set(ctx.var_ranges) == {"step_length", "edep", "delta_e", "post_E"}
|
||||
meta = RunMeta.load(run_dir / "run_meta.json")
|
||||
assert meta.reference.endswith("reference.parquet")
|
||||
assert meta.plot_meta["checkpoint"] == "/ckpt/best.pt"
|
||||
assert "best.pt" in meta.title
|
||||
|
||||
|
||||
def test_compute_one_from_run_dir(tmp_path: Path):
|
||||
run_dir = prep(_write_inputs(tmp_path), **_CTX)
|
||||
out = compute_one("marginal_edep", run_dir)
|
||||
assert out == run_dir / "reduced" / "marginal_edep.json"
|
||||
reduced = Reduced.load(out)
|
||||
assert reduced.id == "marginal_edep"
|
||||
assert len(reduced.payload["rollout"]) == len(reduced.payload["edges"]) - 1
|
||||
|
||||
|
||||
def test_prep_writes_shared_json(tmp_path: Path):
|
||||
r, t = _rollout_frame(), _reference_frame()
|
||||
shared = prep(
|
||||
r,
|
||||
t,
|
||||
tmp_path / "run",
|
||||
n_energy_bins=2,
|
||||
n_marginal_bins=8,
|
||||
top_k_pdg=3,
|
||||
sample_rows=1000,
|
||||
def test_compute_reduced_explicit_paths(tmp_path: Path):
|
||||
run_dir = prep(_write_inputs(tmp_path), **_CTX)
|
||||
meta = RunMeta.load(run_dir / "run_meta.json")
|
||||
out = compute_reduced(
|
||||
"marginal_step_length",
|
||||
meta.rollout,
|
||||
meta.reference,
|
||||
run_dir / "shared.json",
|
||||
tmp_path / "r.json",
|
||||
)
|
||||
assert shared.exists()
|
||||
ctx = Context.load(shared)
|
||||
assert set(ctx.var_ranges) == {"step_length", "edep", "delta_e", "post_E"}
|
||||
assert Reduced.load(out).id == "marginal_step_length"
|
||||
|
||||
|
||||
def test_write_submit_description(tmp_path: Path):
|
||||
cfg = SubmitConfig(
|
||||
rollout=tmp_path / "r.parquet",
|
||||
reference=tmp_path / "t.parquet",
|
||||
out_dir=tmp_path / "run",
|
||||
accounting_group="cms",
|
||||
repo_dir=tmp_path,
|
||||
)
|
||||
sub = write_submit(cfg)
|
||||
txt = sub.read_text()
|
||||
run_dir = prep(_write_inputs(tmp_path), **_CTX)
|
||||
cfg = SubmitConfig(run_dir=run_dir, accounting_group="cms", repo_dir=tmp_path)
|
||||
txt = write_submit(cfg).read_text()
|
||||
assert "universe = docker" in txt
|
||||
assert "docker_image = mschnepf/slc7-condocker" in txt
|
||||
assert "requirements = TARGET.ProvidesETPResources" in txt
|
||||
assert "accounting_group = cms" in txt
|
||||
assert "queue plotid from" in txt
|
||||
# one queue item per catalog id
|
||||
ids = (cfg.out_dir / "plotids.txt").read_text().split()
|
||||
assert ids == catalog_ids()
|
||||
# wrapper is executable and self-contained
|
||||
wrapper = cfg.out_dir / "run_compute.sh"
|
||||
assert (run_dir / "plotids.txt").read_text().split() == catalog_ids()
|
||||
wrapper = run_dir / "run_compute.sh"
|
||||
assert wrapper.exists() and (wrapper.stat().st_mode & 0o111)
|
||||
assert "giant analyze compute-one" in wrapper.read_text()
|
||||
body = wrapper.read_text()
|
||||
assert "giant analyze compute-one --id" in body and "--run-dir" in body
|
||||
|
||||
|
||||
def test_write_submit_remote_flag(tmp_path: Path):
|
||||
run_dir = prep(_write_inputs(tmp_path), **_CTX)
|
||||
cfg = SubmitConfig(
|
||||
rollout=tmp_path / "r.parquet",
|
||||
reference=tmp_path / "t.parquet",
|
||||
out_dir=tmp_path / "run",
|
||||
accounting_group="cms",
|
||||
repo_dir=tmp_path,
|
||||
remote=True,
|
||||
run_dir=run_dir, accounting_group="cms", repo_dir=tmp_path, remote=True
|
||||
)
|
||||
txt = write_submit(cfg).read_text()
|
||||
assert "+RemoteJob = True" in txt
|
||||
|
||||
Reference in New Issue
Block a user