From 98b09d2b4b5431843c166cee2460065d477756fa Mon Sep 17 00:00:00 2001 From: Lars Bogner Date: Fri, 14 Aug 2026 12:22:14 +0200 Subject: [PATCH] Also clip the positive tail of raw predicted log_mass in rollout The previous commit clipped log_mass's negative tail (undershooting _EPS made mass go slightly negative). The mirror case also crashes rollout: a sufficiently large raw predicted log_mass overflows exp() in float32, giving mass = inf, which then fails the same downstream log_transform finiteness check when that mass is fed back in as conditioning for a further step. Bound the upper tail too, at a value comfortably below float32's overflow point. Co-Authored-By: Claude Sonnet 5 --- giant/data/transforms.py | 28 +++++++++++++++++++--------- tests/test_phase2.py | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/giant/data/transforms.py b/giant/data/transforms.py index be57fb6..faf7e58 100644 --- a/giant/data/transforms.py +++ b/giant/data/transforms.py @@ -10,6 +10,14 @@ _EPS = 1e-8 # the conservation it slightly softens is physically negligible (~0.001%). _SIMPLEX_FLOOR = 1e-5 +# Upper clip for a raw predicted log_mass before inv_log_transform: exp(y) +# must stay well inside float32 range (~3.4e38, i.e. y < ~88.7) or it +# overflows to inf, which — like the negative-mass case below — blows up the +# next log_transform call once that mass is fed back in as conditioning. +# 80.0 leaves comfortable headroom while still being far beyond any physical +# particle mass a converged model would ever predict. +_LOG_MASS_MAX = 80.0 + def log_transform(x: np.ndarray, eps: float = _EPS) -> np.ndarray: x = np.asarray(x, dtype=np.float32) @@ -630,15 +638,17 @@ def decode_secondaries( ) # log_mass is a raw model prediction, not itself the output of - # log_transform, so exp(log_mass) can undershoot _EPS and make - # inv_log_transform(log_mass) = exp(log_mass) - _EPS go slightly - # negative. That negative mass then blows up the next log_transform - # call on this track's mass once it's fed back in as conditioning for a - # further rollout step (giant/rollout.py -> build_cond_features -> - # _physical_cond_columns). Clip log_mass so its inverse is guaranteed - # >= 0 before that can happen; clip to 0 separately for padded/invalid - # slots rather than leaving a spurious small positive floor. - log_mass = np.clip(log_mass, np.log(_EPS), None) + # log_transform, so it can land far outside the range that round-trips + # cleanly through inv_log_transform: too negative and exp(log_mass) + # undershoots _EPS, making inv_log_transform go slightly negative; too + # positive and exp(log_mass) overflows float32 to inf. Either one then + # blows up the next log_transform call on this track's mass once it's + # fed back in as conditioning for a further rollout step + # (giant/rollout.py -> build_cond_features -> _physical_cond_columns). + # Clip log_mass to a range whose inverse is guaranteed finite and >= 0 + # before that can happen; clip to 0 separately for padded/invalid slots + # rather than leaving a spurious small positive floor. + log_mass = np.clip(log_mass, np.log(_EPS), _LOG_MASS_MAX) sec_mass = np.where(sec_valid, inv_log_transform(log_mass), 0.0).astype(np.float32) sec_charge = np.where(sec_valid, charge, 0.0).astype(np.float32) diff --git a/tests/test_phase2.py b/tests/test_phase2.py index ac9ba8d..32ae32c 100644 --- a/tests/test_phase2.py +++ b/tests/test_phase2.py @@ -566,3 +566,27 @@ def test_decode_secondaries_extreme_negative_log_mass_stays_nonnegative(): # stay well above -eps so log_transform(mass) stays finite. assert sec_mass[0, 0] > -1e-8 log_transform(sec_mass[0, 0]) + + +def test_decode_secondaries_extreme_positive_log_mass_stays_finite(): + from giant.data.transforms import decode_secondaries, log_transform + + N = 1 + e_sec = np.array([5.0], dtype=np.float32) + n_sec = np.array([1]) + pre_dir = np.array([[0.0, 0.0, 1.0]], dtype=np.float32) + + sec_cont = np.zeros((N, K_MAX, 6), dtype=np.float32) + sec_cont[0, 0, 0] = 10.0 # stick logit -> ~all of e_sec + sec_cont[0, 0, 1:4] = [0, 0, 1] + sec_cont[0, 0, 4] = 200.0 # raw model prediction: extremely positive log_mass + sec_cont[0, 0, 5] = 1.0 + + _, _, sec_mass, _, _ = decode_secondaries(sec_cont, n_sec, e_sec, pre_dir) + + # Mirror image of the extreme-negative case above: exp(log_mass) + # overflows float32 to inf for an unclipped raw prediction this large, + # which then crashes the next log_transform call the same way a + # negative mass would. + assert np.isfinite(sec_mass[0, 0]) + log_transform(sec_mass[0, 0])