lars ebd3e0dc71
CI / Lint (ruff check) (push) Successful in 32s
CI / Format (ruff format) (push) Successful in 30s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 35s
CI / Lint (ruff check) (pull_request) Successful in 33s
CI / Format (ruff format) (pull_request) Successful in 30s
CI / Type check (ty) (pull_request) Successful in 34s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Tests (push) Successful in 5m59s
CI / Bump version, tag, and update changelog on merge to master (push) Has been skipped
CI / Tests (pull_request) Successful in 4m22s
CI / Bump version, tag, and update changelog on merge to master (pull_request) Has been skipped
Add multi-rollout support to giant analyze (gitea #77)
giant analyze compares N rollout YAMLs against one shared reference file
(all must name the same dataset, checked up front) instead of exactly one
rollout vs one reference, rendering each rollout as its own colored series
against a single reference line/panel. Series names come from a repeated
--label flag, else the YAML stem, else "rollout" for a single YAML — a
single-rollout run keeps rendering identically to before this change.

Bundle now holds a name-keyed dict of rollout sides instead of one fixed
pair, every catalog compute_partial/finalize builds a Reduced.payload
keyed the same way ("series": {name: ...}, "reference": ... as the one
distinguished non-rollout entry), and every renderer draws N series (or
N panels, for the two heatmap-shaped specs and the router/type-embedding
diagnostics, which are inherently one-matrix/one-checkpoint per rollout)
against the reference's fixed dashed-ink style.
2026-08-24 13:23:50 +02:00

giant

Geant4 Inference via Autoregressive Neural sTep surrogate.

A conditional generative model that replaces the Geant4 step function: given a pre-step particle state it samples a post-step outcome — the primary's continuation plus its secondary particles — and autoregressively rolls that out into full showers. Trained entirely from parquet dumps of the miniCaloSim steps tree; no Geant4 runtime dependency.

Quick start

uv sync --extra cpu               # install deps (CPU torch; use --extra cuda for GPU)

giant new-run --hidden-dim 512 --lr 3e-4          # scaffold config.toml + run dir
giant train path/to/steps.parquet                 # train (flow + wgan by default)
giant predict path/to/steps.parquet --checkpoint checkpoints/.../best.pt

dwarf build-geometry-oracle path/to/steps.parquet --out oracle.pkl   # needed for rollout
giant rollout path/to/steps.parquet --checkpoint checkpoints/.../best.pt --geometry oracle.pkl

Every command takes --help for the full flag list, and --config config.toml for anything not exposed as a flag.

Architecture

A two-stage model, checkpointed together. Either stage's outcome can be produced by one of three interchangeable generative objectives (--stage1-generator/--stage2-generator, or --mode to set both at once): flow (conditional flow matching, ODE-sampled in ~10 steps), ddpm (denoising diffusion), or wgan (single-pass WGAN-GP generator/critic).

Stage 1 — primary step. Predicts the 9D post-step outcome (giant/constants.py:LOCAL_TARGET_NAMES) from the pre-step conditioning:

Index Variable Encoding
0 step_length [mm] log
12 edep_logit, sec_logit ALR coords of the deposit/secondary/post-energy simplex
35 post_dir in local frame unit vector
68 travel_dir (post_pos pre_pos) in local frame unit vector
  • Energy logits decode via softmax over [edep_logit, sec_logit, 0] × pre_E, so edep + e_sec + post_E == pre_E exactly — conservation is architectural, not learned.
  • post_dir/travel_dir live in the frame where pre_dir = ẑ. post_pos isn't a target — it's reconstructed as pre_pos + step_length * world_frame(travel_dir).

Stage 2 — secondaries. Conditioned on the pre-step state and Stage 1's outcome, it generates the variable-length list of secondary particles. Two decoding strategies (--stage2-decoder):

  • autoregressive — emits secondaries one at a time in descending-energy order, each token conditioned on a running history of prior tokens (markov: previous token only, or attention: causal self-attention, KV-cached at inference)
  • one_shot — all K_MAX slots generated in a single forward pass, masked past the predicted n_sec

Either way, secondary energies stick-break the e_sec budget handed down from Stage 1, so the full chain conserves energy. A secondary's particle identity is represented as onehot (categorical, top-N PDG codes + "other"), physical (continuous log-mass/charge), or embedding (nearest-neighbour lookup).

Conditioning. Pre-step position/energy/direction/layer, plus particle mass/charge and material Z_eff/A_eff/density/X0/λ_int, encoded the same three ways as particle identity above (--conditioning) — the physical representation generalizes to species/materials outside the training menu since it's computed rather than looked up. n_sec/e_sec are always model outputs, never conditioning inputs.

MoE routing (--router, either stage): a pluggable Router (energy/pdg/process/composed axes) top-1-dispatches each row to one of several small expert trunks at eval time, instead of running one monolithic trunk.

Data

  • Input: parquet files produced by miniCaloSim, or converted from ROOT via dwarf convert. One row = one Geant4 step.
  • Conditioning (pre-step) columns: event_id, pdg, pre_x/pre_y/pre_z, pre_E, pre_dx/pre_dy/pre_dz (direction), material, layer_id.
  • Primary outcome (post-step) columns: post_x/post_y/post_z, post_E, post_dx/post_dy/post_dz, step_length, edep (energy deposited in this step), e_sec (total energy carried off by secondaries), child_track_ids (its length gives n_sec).
  • Secondary columns, one variable-length list per step: sec_pdg_list, sec_E_list, sec_dx_list/sec_dy_list/sec_dz_list — padded/truncated to K_MAX (15) slots on load, ordered by descending energy.
  • Optional: process — the physics process that produced the step (e.g. compt, phot, eBrem); a post-step label used only as classifier supervision (ProcessRouter), never as conditioning.
  • Train/val split is by event_id (--seed-controlled), not row shuffle, so correlated steps from the same shower never leak across the split.
  • Loading a directory or .manifest of multiple parquet files (each one Geant4 job, event_id restarting from 0) offsets each file's event_ids by a fixed per-file stride so ids stay globally unique across files.

Project structure

giant/
├── giant/
│   ├── data/
│   │   ├── loader.py       # parquet → numpy arrays (incl. streaming/chunked reads)
│   │   ├── transforms.py   # log transforms, local-frame rotation, energy simplex, secondary encode/decode
│   │   └── dataset.py      # StepsDataset / StreamingStepsDataset (PyTorch)
│   ├── model/
│   │   ├── network.py      # ConditionEncoder, Stage1Model, Stage2OneShot/Stage2Autoregressive, Router/MoE, CriticModel
│   │   ├── schedule.py     # CosineSchedule (DDPM) and flow matching utilities
│   │   └── wgan.py         # WGAN-GP gradient penalty / critic / generator losses
│   ├── constants.py        # output/conditioning dims, K_MAX, secondary slot layout, schema keys
│   ├── particles.py        # PDG → (mass, charge) decode, incl. nuclear/ion codes; onehot/embedding secondary-identity decode
│   ├── materials.py        # material name → (Z_eff, A_eff, density, X0, λ_int)
│   ├── config.py           # default hyperparameters, TOML config merging, device autodetect
│   ├── pipeline.py         # builds datasets/normalizers and kicks off a training run (with setup-stage caching)
│   ├── training/           # two-stage training: loop, per-stage trainers, metrics, checkpointing
│   │   ├── loop.py         #   epoch loop, graceful shutdown, best-checkpoint selection
│   │   ├── trainers.py     #   StageSpec + flow/ddpm and WGAN-GP per-stage trainers
│   │   ├── stage2_inputs.py#   ground-truth stage-2 targets + autoregressive/teacher-forcing inputs
│   │   ├── metrics.py      #   MetricsCollector: metrics.csv columns, W&B logging, progress/summary
│   │   └── checkpoint.py   #   checkpoint assembly/restore (format unchanged since v0.2)
│   ├── sample.py           # DDPM / DDIM / flow matching / WGAN samplers + secondary sampling
│   ├── geometry.py         # GeometryOracle: position → (material, layer_id, escaped) for rollout
│   ├── rollout.py          # autoregressive shower rollout driver
│   ├── validate.py         # step-level marginal + KL-divergence validation
│   ├── analysis/           # rollout-vs-reference analysis pipeline (see `giant analyze` below)
│   │   ├── sources.py      #   canonical LazyFrames + secondary view
│   │   ├── reduce.py       #   streaming reduction primitives (hist1d, per-event scalars, profiles, ...)
│   │   ├── grouping.py     #   fixed bin edges + energy/pdg/material group sets
│   │   ├── context.py      #   resolves grouping into `shared.json` once per run
│   │   ├── catalog.py      #   declarative PlotSpec registry
│   │   ├── condor.py       #   prep / compute-one / submit-description plumbing
│   │   └── render.py       #   PDFs + HTML gallery (only module importing plotstyle/LaTeX)
│   └── cli.py              # `giant train` / `new-run` / `predict` / `rollout` / `analyze` Typer app
├── giant/tools/                        # dataset/tooling logic, unified under the `dwarf` CLI (`dwarf --help`)
│   ├── dwarf.py                        # Typer app: convert, migrate, bump-gen, bump-schema, status,
│   │                                    #   update-manifest, create-manifest, make-root,
│   │                                    #   build-geometry-oracle, warm-cache, hparam-scan
│   ├── steps_to_parquet.py             # ROOT → parquet conversion (uproot/awkward/polars) — `dwarf convert`
│   ├── steps_to_parquet_parallel.py    # fan out conversion over several ROOT files — `dwarf convert --jobs N`
│   ├── migrate_geant_steps.py          # one-time move into the raw/processed/pools/derived layout — `dwarf migrate`
│   ├── bump_dataset_version.py         # cut a new raw gen or parquet schema, with a logged reason —
│   │                                    #   `dwarf bump-gen` / `bump-schema` / `status` / `update-manifest` / `create-manifest`
│   ├── create_root_files.py            # generate new ROOT shards via a minicalosim executable — `dwarf make-root`
│   ├── geometry_oracle.py              # fit a position → (material, layer_id) oracle — `dwarf build-geometry-oracle`
│   ├── warm_setup_cache.py             # precompute `giant train`'s setup-stage sidecar — `dwarf warm-cache`
│   ├── hparam_scan.py                  # hyperparameter grid scan over `giant train` runs — `dwarf hparam-scan`
│   └── profile_analysis_costs.py       # profiling helper for the `giant analyze` reduction pipeline
└── tests/

Setup

uv sync --extra cpu                   # CPU-only torch (use --extra cuda for CUDA 11.8 instead)
uv sync --extra cpu --extra dev       # add dev tools (pytest, ruff, ty)
uv sync --extra cpu --extra geometry  # add scikit-learn, for `dwarf build-geometry-oracle` / rollout

cpu and cuda are mutually exclusive — pick one to select the torch build (pinned to 2.3.x). Plain uv sync installs no torch at all. See CLAUDE.md for details.

Training, prediction, rollout

giant new-run --hidden-dim 512 --lr 3e-4 --comment "..."   # scaffold a config.toml + run dir
giant train path/to/steps.parquet                          # train (flow stage 1 + wgan stage 2, default)
giant predict path/to/steps.parquet --checkpoint checkpoints/.../best.pt

dwarf build-geometry-oracle path/to/steps.parquet --out oracle.pkl   # position → material/layer_id
giant rollout path/to/steps.parquet --checkpoint checkpoints/.../best.pt --geometry oracle.pkl

Useful flags on giant train:

  • --mode {flow,ddpm,wgan} sets both stages' objective at once; --stage1-generator/--stage2-generator override per stage
  • --stage2-decoder {autoregressive,one_shot} — Stage 2 decoding strategy (see Architecture)
  • --conditioning {physical,embedding,onehot} — conditioning representation
  • --router / --router-type / --n-experts / --router-axis — MoE routing
  • --wandb — log per-epoch metrics to Weights & Biases (needs uv sync --extra wandb); metric names are <stage>/<split>/<metric> plus an unprefixed run-level tail, all derived from giant/training/trainers.py MetricSpecs
  • --no-cache-setup / --rebuild-setup-cache — control the setup-stage sidecar cache (vocab maps, event split, normalizer stats); dwarf warm-cache precomputes it
  • --stage1-init-from/--stage2-init-from (checkpoint .pt) + --stage1-freeze/--stage2-freeze — load a stage's weights from another checkpoint and never update them, so the other stage can be retrained alone against a fixed, known-good one while still producing a complete, rollout-capable checkpoint

Config-file-only knobs (no CLI flag — use --config config.toml): stage2_model.autoregressive.teacher_forcing/.history, stage2_model.particle_type.target. v0.2 flat-schema configs and checkpoints load fine (auto-migrated).

giant rollout seeds showers from each event's highest-energy entry step, then autoregressively steps the model to completion, pushing secondaries as new tracks and looking up material/layer_id from the geometry oracle each step. Tracks terminate on energy cutoff, max steps, detector escape, or natural end; energy is deposited locally on every stop except escape, so showers conserve energy by construction.

Validation and analysis

  • giant.validate.validate_marginals — step-level marginal + KL-divergence checks during training (--validate-every)
  • giant analyze — deeper rollout-vs-reference diagnostics (marginals by energy/pdg/material, per-event totals, shower profiles, species share, leakage, secondaries):
giant analyze submit rollout.yaml --accounting-group cms   # prep + one HTCondor job per plot (compute only)
giant analyze submit a.yaml b.yaml --accounting-group cms --label flow --label wgan  # N rollouts vs one shared reference
giant analyze render <run_dir> --gallery                   # local: styled PDFs + HTML gallery (needs LaTeX)

<run_dir> is derived next to the first rollout's parquet (analyze prep/submit print it). Multiple rollout YAMLs must all name the same reference (dataset) file; each renders as its own colored series against one reference line/panel. Compute jobs are polars/numpy only; only render needs LaTeX, so it always runs locally.

Development

uv run pytest            # run tests
uv run ruff check .      # lint
uv run ruff format .     # format
uv run ty check .        # type check
S
Description
GIANT — conditional generative surrogate for the Geant4 step function. Two-stage flow-matching model that replaces Geant4's stochastic shower physics, predicting post-step outcomes and secondary particles while conserving energy by construction.
Readme 17 MiB
Languages
Python 100%