Also clip the positive tail of raw predicted log_mass in rollout
CI / Format (ruff format) (push) Failing after 29s
CI / Lint (ruff check) (push) Successful in 30s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 27s
CI / Tests (push) Successful in 1m10s

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:22:14 +02:00
parent d0381728ee
commit 98b09d2b4b
2 changed files with 43 additions and 9 deletions
+19 -9
View File
@@ -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)
+24
View File
@@ -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])