Files
giant/tests/test_router_gating.py
lars 60c2ca1985
CI / Lint (ruff check) (push) Successful in 58s
CI / Format (ruff format) (push) Successful in 1m3s
CI / Type check (ty) (push) Successful in 1m10s
CI / Tests (push) Successful in 1m56s
CI / Bump version, build & publish wheel (push) Has been skipped
analyze: add MoE router gating/share diagnostic plots
New "model" family in the gallery: router_gating (mean soft gate weight
vs. pre-step energy, showing the router's soft decision boundaries) and
router_share_by_pdg/router_share_by_process (stacked top-1 dispatch share
by species / true physics process). Needs a live checkpoint's Router, so
it's a documented exception to the rest of the package's polars/numpy-only
contract; gracefully degrades to a placeholder for non-MoE checkpoints.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 13:32:25 +02:00

127 lines
3.8 KiB
Python

"""Tests for the MoE router-gating diagnostic (giant.analysis.router_gating)."""
from __future__ import annotations
import numpy as np
import polars as pl
import torch
from giant.analysis.router_gating import (
compute_router_gating,
compute_router_share_by_pdg,
compute_router_share_by_process,
)
from giant.data.transforms import Normalizer
from giant.model.network import build_models
_PDG_MAP = {11: 0, 22: 1}
_MAT_MAP = {"G4_PbWO4": 0, "G4_Pb": 1}
def _model_cfg() -> dict:
return {
"router": {
"enabled": True,
"type": "energy",
"n_experts": 2,
"temperature": 0.5,
"learn_centers": True,
"energy_idx": 3,
},
"pdg_vocab": len(_PDG_MAP),
"mat_vocab": len(_MAT_MAP),
"conditioning": "embedding",
}
def _write_checkpoint(tmp_path) -> str:
cfg = _model_cfg()
stage1, _ = build_models(cfg)
norm = Normalizer()
norm.mean = np.zeros(15, dtype=np.float32)
norm.std = np.ones(15, dtype=np.float32)
ckpt = {
"model_config": cfg,
"model": stage1.state_dict(),
"pdg_map": _PDG_MAP,
"mat_map": _MAT_MAP,
"normalizer": {"cond": norm.to_dict()},
}
path = tmp_path / "ckpt.pt"
torch.save(ckpt, path)
return str(path)
def _steps_frame(process: bool = False) -> pl.LazyFrame:
n = 40
rng = np.random.default_rng(0)
pre_e = np.concatenate([rng.uniform(1, 10, n // 2), rng.uniform(100, 1000, n // 2)])
pdg = np.where(np.arange(n) % 2 == 0, 11, 22)
material = np.where(np.arange(n) % 3 == 0, "G4_Pb", "G4_PbWO4")
data = {
"event_id": np.arange(n),
"pdg": pdg,
"pre_x": np.zeros(n),
"pre_y": np.zeros(n),
"pre_z": np.zeros(n),
"pre_E": pre_e,
"pre_dx": np.zeros(n),
"pre_dy": np.zeros(n),
"pre_dz": np.ones(n),
"post_x": np.zeros(n),
"post_y": np.zeros(n),
"post_z": np.ones(n),
"post_E": pre_e * 0.5,
"post_dx": np.zeros(n),
"post_dy": np.zeros(n),
"post_dz": np.ones(n),
"edep": pre_e * 0.5,
"step_length": np.ones(n),
"material": material,
"layer_id": np.zeros(n, dtype=np.int64),
}
if process:
data["process"] = np.where(pdg == 11, "eIoni", "compt")
return pl.DataFrame(data).lazy()
def test_compute_router_gating_shapes(tmp_path):
checkpoint = _write_checkpoint(tmp_path)
lf = _steps_frame()
r = compute_router_gating(checkpoint, lf, lf)
assert r.kind == "router_gating"
assert r.payload["n_experts"] == 2
for side in ("rollout", "reference"):
means = r.payload[side]["means"]
assert means, f"{side} produced no bins"
assert all(abs(sum(row) - 1.0) < 1e-5 for row in means)
def test_compute_router_gating_missing_checkpoint_is_unavailable():
lf = _steps_frame()
r = compute_router_gating(None, lf, lf)
assert r.kind == "unavailable"
assert "note" in r.payload
assert r.title
def test_compute_router_share_by_pdg(tmp_path):
checkpoint = _write_checkpoint(tmp_path)
lf = _steps_frame()
r = compute_router_share_by_pdg(checkpoint, lf, lf, top_pdgs=[11, 22])
assert r.kind == "router_share"
for side in ("rollout", "reference"):
assert set(r.payload[side]) == {"e-", "gamma"}
for shares in r.payload[side].values():
assert abs(sum(shares) - 1.0) < 1e-5
def test_compute_router_share_by_process(tmp_path):
checkpoint = _write_checkpoint(tmp_path)
lf = _steps_frame(process=True)
r = compute_router_share_by_process(checkpoint, lf)
assert r.kind == "router_share"
assert set(r.payload["categories"]) <= {"eIoni", "compt"}
for shares in r.payload["reference"].values():
assert abs(sum(shares) - 1.0) < 1e-5