Fix crashes in physical-property conditioning edge cases
- make_seed_frontier only resolves particle mass/charge in "physical" mode, so "embedding"-mode rollouts no longer crash on a seed PDG code giant.particles can't resolve (the TERM_UNKNOWN_PDG gate now handles it). - nearest_known_pdg skips unresolvable candidate PDG codes instead of raising and killing the whole rollout/predict run. - predict/rollout fail with a clear message when a checkpoint predates the sec_phys normalizer, instead of a bare KeyError. - validate_marginals' phys_kl degrades to NaN (matching the energy_fraction_kl pattern) instead of crashing when a validated batch has zero secondaries on either side. - Correct CLAUDE.md's stale claim that the materials table is unfilled. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,16 @@ def test_nearest_known_pdg_empty_candidates_raises():
|
||||
nearest_known_pdg(np.array([1.0]), np.array([0.0]), [])
|
||||
|
||||
|
||||
def test_nearest_known_pdg_skips_unresolvable_candidate():
|
||||
"""One unresolvable code in the candidate set (e.g. a training-vocab
|
||||
entry giant.particles can't decode) must not crash the lookup -- it's
|
||||
simply excluded from the nearest-neighbour candidate pool."""
|
||||
candidates = [22, 11, -11, 999999999]
|
||||
mass_e, charge_e = particle_mass_charge(11)
|
||||
result = nearest_known_pdg(np.array([mass_e]), np.array([charge_e]), candidates)
|
||||
assert result[0] == 11
|
||||
|
||||
|
||||
def test_nearest_known_pdg_shape():
|
||||
candidates = [22, 11, -11, 2212, 2112]
|
||||
n = 10
|
||||
|
||||
+24
-1
@@ -8,7 +8,7 @@ import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from giant.constants import TERM_ESCAPED, TERM_MAX_STEPS
|
||||
from giant.constants import TERM_ESCAPED, TERM_MAX_STEPS, TERM_UNKNOWN_PDG
|
||||
from giant.data.transforms import Normalizer
|
||||
from giant.model.network import DenoisingMLP, SecondaryDecoder
|
||||
from giant.rollout import make_seed_frontier, rollout
|
||||
@@ -130,6 +130,29 @@ def test_seed_frontier_track_ids():
|
||||
np.testing.assert_allclose(np.linalg.norm(fr["pre_dir"], axis=1), 1.0, atol=1e-6)
|
||||
|
||||
|
||||
def test_seed_frontier_embedding_mode_skips_unresolvable_pdg_lookup():
|
||||
""" "embedding" mode must not call giant.particles at all, so a seed PDG
|
||||
code it can't resolve (e.g. a fabricated/garbage code) must not crash
|
||||
frontier construction — mass/charge are simply zero-filled, unused."""
|
||||
seeds = _seeds(3)
|
||||
seeds["pdg"] = np.full(3, 999999999, dtype=np.int64)
|
||||
fr, _counts = make_seed_frontier(**seeds, conditioning="embedding")
|
||||
np.testing.assert_array_equal(fr["mass"], 0.0)
|
||||
np.testing.assert_array_equal(fr["charge"], 0.0)
|
||||
|
||||
|
||||
def test_rollout_embedding_mode_unresolvable_pdg_terminates_gracefully():
|
||||
"""A rollout seeded with a PDG code giant.particles can't resolve, and
|
||||
which isn't in the training vocabulary either, must terminate via the
|
||||
existing TERM_UNKNOWN_PDG gate rather than crash in make_seed_frontier —
|
||||
"embedding" mode has no dependency on giant.particles at all."""
|
||||
seeds = _seeds(3)
|
||||
seeds["pdg"] = np.full(3, 999999999, dtype=np.int64)
|
||||
rec = _run(seeds=seeds, conditioning="embedding")
|
||||
assert len(rec["event_id"]) > 0
|
||||
assert set(rec["termination_reason"].tolist()) == {TERM_UNKNOWN_PDG}
|
||||
|
||||
|
||||
def test_rollout_terminates_and_has_rows():
|
||||
rec = _run()
|
||||
assert len(rec["event_id"]) > 0
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from giant.constants import COND_DIM, K_MAX, SEC_SLOT_DIM, X_DIM
|
||||
from giant.model.network import DenoisingMLP, SecondaryDecoder
|
||||
from giant.validate import validate_marginals
|
||||
|
||||
|
||||
def _tiny_models():
|
||||
s1 = DenoisingMLP(pdg_vocab=3, mat_vocab=2, hidden_dim=16, n_blocks=1)
|
||||
s2 = SecondaryDecoder(pdg_vocab=3, mat_vocab=2, hidden_dim=16, n_blocks=1)
|
||||
return s1.eval(), s2.eval()
|
||||
|
||||
|
||||
def _zero_secondaries_loader(B=4, n_batches=2):
|
||||
"""A val_loader whose every batch has n_sec=0 (real side) — matches the
|
||||
(cond_cont, cond_cat, target_s1, n_sec, sec_cont, proc_idx) tuple shape
|
||||
StreamingStepsDataset yields."""
|
||||
batches = []
|
||||
for _ in range(n_batches):
|
||||
cond_cont = torch.randn(B, COND_DIM)
|
||||
cond_cat = torch.zeros(B, 2, dtype=torch.long)
|
||||
x1 = torch.randn(B, X_DIM)
|
||||
n_sec = torch.zeros(B, dtype=torch.long)
|
||||
sec_cont = torch.zeros(B, K_MAX, SEC_SLOT_DIM)
|
||||
proc_idx = torch.zeros(B, dtype=torch.long)
|
||||
batches.append((cond_cont, cond_cat, x1, n_sec, sec_cont, proc_idx))
|
||||
return batches
|
||||
|
||||
|
||||
def test_validate_marginals_all_zero_secondaries_returns_nan_phys_kl(monkeypatch):
|
||||
"""If n_sec_pred collapses to 0 across the whole validated set (realistic
|
||||
during early/unstable training), phys_kl must degrade to NaN instead of
|
||||
crashing on the empty-array .min()/.max() reduction inside
|
||||
_histogram_kl -- a regression the old species/bincount code this
|
||||
replaced explicitly guarded against."""
|
||||
s1, s2 = _tiny_models()
|
||||
loader = _zero_secondaries_loader()
|
||||
|
||||
# Force the Stage-1 n_sec head's prediction to 0 for every sample too, so
|
||||
# the generated side's valid-slot mask is also empty (real side is
|
||||
# already all n_sec=0 by construction of the fake loader above).
|
||||
def _fake_sample_flow(model, cond_cont, cond_cat, **kw):
|
||||
B = cond_cont.size(0)
|
||||
return torch.randn(B, X_DIM), torch.zeros(B, dtype=torch.long)
|
||||
|
||||
monkeypatch.setattr("giant.validate.sample_flow", _fake_sample_flow)
|
||||
|
||||
result = validate_marginals(s1, loader, sec_decoder=s2, n_batches=2)
|
||||
|
||||
assert np.asarray(result["phys_real"]).shape == (0, 2)
|
||||
assert np.asarray(result["phys_generated"]).shape == (0, 2)
|
||||
assert np.isnan(np.asarray(result["phys_kl"])).all()
|
||||
Reference in New Issue
Block a user