Add load_rollout_vs_truth to compare rollouts against held-out truth data
Extends the Tier 1-3 SampleCollection diagnostics (marginals, correlations, pairwise, direction alignment, constraints) to work on a full autoregressive giant rollout shower checked against an independent ground-truth steps file, rather than only paired giant predict --coord local output. The two files are unpaired (different lengths, own conditioning), so SampleCollection gains optional *_gen fields and _group_labels/marginal_table/plot_marginals/ plot_pairwise build independent real/gen masks instead of assuming one. Adds analysis/rollout_validation.ipynb, a sibling of validation.ipynb built around this workflow. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ from giant.analysis import (
|
||||
correlation_matrices,
|
||||
direction_alignment,
|
||||
load_predicted_local,
|
||||
load_rollout_vs_truth,
|
||||
marginal_table,
|
||||
marginal_table_pl,
|
||||
pdg_contribution_table_pl,
|
||||
@@ -40,12 +41,15 @@ from giant.constants import (
|
||||
PREDICT_COORD_METADATA_KEY,
|
||||
PREDICT_SCHEMA_VERSION,
|
||||
PREDICT_SCHEMA_VERSION_KEY,
|
||||
ROLLOUT_COORD_VALUE,
|
||||
)
|
||||
from giant.data.transforms import (
|
||||
energy_simplex_decode,
|
||||
inv_log_transform,
|
||||
local_frame_rotation,
|
||||
log_transform,
|
||||
reconstruct_post_pos,
|
||||
travel_direction,
|
||||
)
|
||||
|
||||
|
||||
@@ -660,3 +664,177 @@ def test_plot_pdg_energy_share_caps_slices():
|
||||
fig = plot_pdg_energy_share(table, max_slices=4)
|
||||
for ax in fig.axes:
|
||||
assert len(ax.patches) == 4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# load_rollout_vs_truth: unpaired rollout-vs-truth SampleCollection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_world_frame_physical(rng, n):
|
||||
"""Random-but-physical pre/post step fields shared by the truth/rollout schemas."""
|
||||
pre_pos = rng.uniform(-5.0, 5.0, (n, 3)).astype(np.float32)
|
||||
pre_dir = _unit_vectors(rng, n)
|
||||
pre_E = rng.uniform(1.0, 100.0, n).astype(np.float32)
|
||||
step_length = rng.uniform(0.1, 5.0, n).astype(np.float32)
|
||||
travel_dir_world = _unit_vectors(rng, n)
|
||||
post_pos = pre_pos + step_length[:, None] * travel_dir_world
|
||||
post_dir_world = _unit_vectors(rng, n)
|
||||
delta_e = (rng.uniform(0.0, 1.0, n) * pre_E).astype(np.float32)
|
||||
post_E = pre_E - delta_e
|
||||
edep = (delta_e * rng.uniform(0.0, 1.0, n)).astype(np.float32)
|
||||
return pre_pos, pre_dir, pre_E, step_length, post_pos, post_dir_world, post_E, edep
|
||||
|
||||
|
||||
def _expected_raw9(
|
||||
pre_pos, pre_dir, pre_E, step_length, post_pos, post_dir_world, post_E, edep
|
||||
):
|
||||
post_dir_local = local_frame_rotation(pre_dir, post_dir_world)
|
||||
travel_dir_local = local_frame_rotation(
|
||||
pre_dir, travel_direction(pre_pos, post_pos)
|
||||
)
|
||||
return np.column_stack(
|
||||
[step_length, pre_E - post_E, edep, post_dir_local, travel_dir_local]
|
||||
).astype(np.float32)
|
||||
|
||||
|
||||
def _write_truth_parquet(path, n=200, seed=0):
|
||||
rng = np.random.default_rng(seed)
|
||||
fields = _make_world_frame_physical(rng, n)
|
||||
pre_pos, pre_dir, pre_E, step_length, post_pos, post_dir_world, post_E, edep = (
|
||||
fields
|
||||
)
|
||||
table = pa.table(
|
||||
{
|
||||
"event_id": rng.integers(0, 20, n),
|
||||
"pdg": rng.choice([11, -11, 22], n),
|
||||
"pre_x": pre_pos[:, 0],
|
||||
"pre_y": pre_pos[:, 1],
|
||||
"pre_z": pre_pos[:, 2],
|
||||
"pre_E": pre_E,
|
||||
"pre_dx": pre_dir[:, 0],
|
||||
"pre_dy": pre_dir[:, 1],
|
||||
"pre_dz": pre_dir[:, 2],
|
||||
"material": rng.choice(["W", "Pb"], n),
|
||||
"layer_id": rng.integers(0, 10, n).astype(np.int32),
|
||||
"child_track_ids": [list(range(int(k))) for k in rng.integers(0, 3, n)],
|
||||
"e_sec": rng.uniform(0.0, 1.0, n).astype(np.float32),
|
||||
"step_length": step_length,
|
||||
"post_E": post_E,
|
||||
"edep": edep,
|
||||
"post_dx": post_dir_world[:, 0],
|
||||
"post_dy": post_dir_world[:, 1],
|
||||
"post_dz": post_dir_world[:, 2],
|
||||
"post_x": post_pos[:, 0],
|
||||
"post_y": post_pos[:, 1],
|
||||
"post_z": post_pos[:, 2],
|
||||
}
|
||||
)
|
||||
pq.write_table(table, path)
|
||||
return _expected_raw9(*fields)
|
||||
|
||||
|
||||
def _write_rollout_parquet(path, n=150, seed=1, coord=ROLLOUT_COORD_VALUE):
|
||||
rng = np.random.default_rng(seed)
|
||||
fields = _make_world_frame_physical(rng, n)
|
||||
pre_pos, pre_dir, pre_E, step_length, post_pos, post_dir_world, post_E, edep = (
|
||||
fields
|
||||
)
|
||||
table = pa.table(
|
||||
{
|
||||
"event_id": rng.integers(0, 20, n),
|
||||
"track_id": rng.integers(0, 3, n),
|
||||
"parent_id": np.full(n, -1, dtype=np.int64),
|
||||
"generation": np.zeros(n, dtype=np.int64),
|
||||
"step_no": np.zeros(n, dtype=np.int64),
|
||||
"pdg": rng.choice([11, -11, 22], n),
|
||||
"pre_x": pre_pos[:, 0],
|
||||
"pre_y": pre_pos[:, 1],
|
||||
"pre_z": pre_pos[:, 2],
|
||||
"pre_E": pre_E,
|
||||
"pre_dx": pre_dir[:, 0],
|
||||
"pre_dy": pre_dir[:, 1],
|
||||
"pre_dz": pre_dir[:, 2],
|
||||
"post_x": post_pos[:, 0],
|
||||
"post_y": post_pos[:, 1],
|
||||
"post_z": post_pos[:, 2],
|
||||
"post_E": post_E,
|
||||
"post_dx": post_dir_world[:, 0],
|
||||
"post_dy": post_dir_world[:, 1],
|
||||
"post_dz": post_dir_world[:, 2],
|
||||
"edep": edep,
|
||||
"step_length": step_length,
|
||||
"material": rng.choice(["W", "Pb"], n),
|
||||
"layer_id": rng.integers(0, 10, n).astype(np.int32),
|
||||
"n_sec_pred": rng.integers(0, 3, n).astype(np.int32),
|
||||
"termination_reason": rng.choice(["natural_end", "energy_cutoff"], n),
|
||||
}
|
||||
)
|
||||
if coord is not None:
|
||||
table = table.replace_schema_metadata({PREDICT_COORD_METADATA_KEY: coord})
|
||||
pq.write_table(table, path)
|
||||
return _expected_raw9(*fields)
|
||||
|
||||
|
||||
def test_load_rollout_vs_truth_decodes_raw_targets_correctly(tmp_path):
|
||||
truth_path = tmp_path / "truth.parquet"
|
||||
rollout_path = tmp_path / "rollout.parquet"
|
||||
expected_real = _write_truth_parquet(truth_path, n=200, seed=0)
|
||||
expected_gen = _write_rollout_parquet(rollout_path, n=150, seed=1)
|
||||
|
||||
samples = load_rollout_vs_truth(rollout_path, truth_path)
|
||||
|
||||
np.testing.assert_allclose(samples.real_raw, expected_real, atol=1e-4)
|
||||
np.testing.assert_allclose(samples.gen_raw, expected_gen, atol=1e-4)
|
||||
|
||||
|
||||
def test_load_rollout_vs_truth_allows_unpaired_lengths(tmp_path):
|
||||
truth_path = tmp_path / "truth.parquet"
|
||||
rollout_path = tmp_path / "rollout.parquet"
|
||||
_write_truth_parquet(truth_path, n=200)
|
||||
_write_rollout_parquet(rollout_path, n=150)
|
||||
|
||||
samples = load_rollout_vs_truth(rollout_path, truth_path)
|
||||
|
||||
assert samples.real_raw.shape == (200, 9)
|
||||
assert samples.gen_raw.shape == (150, 9)
|
||||
assert samples.pdg.shape == (200,)
|
||||
assert samples.pdg_gen.shape == (150,)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_by", [None, "pdg", "material", "energy"])
|
||||
def test_load_rollout_vs_truth_downstream_plots_run_without_error(tmp_path, group_by):
|
||||
truth_path = tmp_path / "truth.parquet"
|
||||
rollout_path = tmp_path / "rollout.parquet"
|
||||
_write_truth_parquet(truth_path, n=200)
|
||||
_write_rollout_parquet(rollout_path, n=150)
|
||||
samples = load_rollout_vs_truth(rollout_path, truth_path)
|
||||
|
||||
table = marginal_table(samples, group_by=group_by)
|
||||
assert set(table["dim"]) == set(RAW_TARGET_NAMES)
|
||||
assert plot_marginals(samples, group_by=group_by) is not None
|
||||
assert plot_kl_bars(samples, group_by=group_by) is not None
|
||||
|
||||
|
||||
def test_load_rollout_vs_truth_joint_and_constraint_checks_run(tmp_path):
|
||||
truth_path = tmp_path / "truth.parquet"
|
||||
rollout_path = tmp_path / "rollout.parquet"
|
||||
_write_truth_parquet(truth_path, n=200)
|
||||
_write_rollout_parquet(rollout_path, n=150)
|
||||
samples = load_rollout_vs_truth(rollout_path, truth_path)
|
||||
|
||||
assert plot_correlation_matrices(samples) is not None
|
||||
assert plot_pairwise(samples) is not None
|
||||
assert plot_direction_alignment(samples) is not None
|
||||
assert plot_constraint_violations(samples) is not None
|
||||
assert constraint_report(samples) is not None
|
||||
|
||||
|
||||
def test_load_rollout_vs_truth_rejects_wrong_coord_metadata(tmp_path):
|
||||
truth_path = tmp_path / "truth.parquet"
|
||||
rollout_path = tmp_path / "rollout.parquet"
|
||||
_write_truth_parquet(truth_path)
|
||||
_write_rollout_parquet(rollout_path, coord="local")
|
||||
|
||||
with pytest.raises(ValueError, match="not a rollout file"):
|
||||
load_rollout_vs_truth(rollout_path, truth_path)
|
||||
|
||||
Reference in New Issue
Block a user