Files
giant/giant/analysis/reduced.py
T
lars 9fa6420183
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 2m3s
CI / Sync project version with tag (pull_request) Has been skipped
CI / Type check (ty) (pull_request) Successful in 1m19s
CI / Lint (ruff check) (push) Successful in 3m55s
CI / Format (ruff format) (push) Successful in 3m54s
CI / Format (ruff format) (pull_request) Successful in 5m37s
CI / Lint (ruff check) (pull_request) Successful in 5m42s
CI / Tests (push) Successful in 8m42s
CI / Bump version, tag, and update changelog on merge to master (push) Has been skipped
CI / Tests (pull_request) Successful in 5m3s
CI / Bump version, tag, and update changelog on merge to master (pull_request) Has been skipped
feat(analysis): per-step secondary multiplicity plots
Replace the event-level n_sec confusion matrix with two step-resolved
secondary-multiplicity comparisons:

- sec_count_per_step: overlay histogram of how many secondaries a single
  step emits, rollout series vs reference.
- sec_count_per_step_by_species: heatmap of per-step multiplicity of one
  species (zero row included) against species, drawn as one panel per
  rollout plus a reference panel, raw counts on a log color scale.

Both are backed by a new sources.secondaries_by_step view, which tags each
secondary with its emitting step — (event_id, parent_id, birth position)
on the rollout side, the row index on the reference side — so neither plot
needs a join against the step frame. Steps that emitted nothing are
recovered by subtraction from the chunk's step count, keeping both specs
sum-mergeable across condor chunks.

The rollout multiplicity is derived from the actual secondary birth rows
rather than the n_sec_pred column, which records the predicted count
before the per-event max-tracks cap.

_render_heatmap gained reference-panel and log-color support;
marginal_distance_summary sets neither key and is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 14:13:18 +02:00

75 lines
2.9 KiB
Python

"""The compact, self-describing artifact a compute job produces per plot.
Serialized as small JSON (no pickle, no per-event arrays) so it is trivially
transferable off the batch worker and human-inspectable. ``render.py`` dispatches
on ``kind`` and needs nothing but this file.
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
# Reduced.kind values (payload keys a rollout series by name under
# payload["series"], with the reference — where one exists — kept as one
# distinguished payload["reference"] entry; see catalog.py's module
# docstring for the full per-kind payload shape):
# "overlay_hist" N-rollout-series vs reference density histogram over shared edges
# "grouped_hist" one panel per group (energy/pdg/material), each an overlay
# "profile" edep-weighted mean +/- event-RMS vs depth/radius, N series + reference
# "bar" per-category N-rollout-series vs reference bars (share / counts)
# "single_hist" rollout-only series (e.g. leakage; reference has none)
# "router_gating" stacked mean MoE gate weight vs energy, one rollout+reference
# panel-pair per rollout with an enabled MoE router
# "router_share" stacked bar of MoE top-1 dispatch share by category, one
# panel per rollout with an enabled MoE router
# "router_specialization" max gate weight vs energy (one scalar trend line
# summarizing "router_gating"), per rollout with an enabled router
# "heatmap" row x col matrix + colorbar, one panel per rollout (a
# distance scorecard)
# "unavailable" plot not applicable to this run (e.g. no MoE checkpoint)
@dataclass
class Reduced:
id: str
family: str
kind: str
title: str
xlabel: str
payload: dict
meta: dict = field(default_factory=dict)
def save(self, path: str | Path) -> None:
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_text(json.dumps(asdict(self)))
@classmethod
def load(cls, path: str | Path) -> "Reduced":
return cls(**json.loads(Path(path).read_text()))
@dataclass
class Partial:
"""The raw, not-yet-finalized output of one ``(plot, chunk)`` compute job.
``data`` holds whatever shape that plot's ``PlotSpec.compute_partial``
returns — a raw sum-mergeable count dict, or a raw per-event/per-secondary
array to be concatenated across chunks — never a finished histogram/profile.
``PlotSpec.finalize`` is the only thing that knows how to interpret it.
"""
id: str
family: str
chunk: int
data: dict
def save(self, path: str | Path) -> None:
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_text(json.dumps(asdict(self)))
@classmethod
def load(cls, path: str | Path) -> "Partial":
return cls(**json.loads(Path(path).read_text()))