Fix negative secondary mass crashing log_transform during rollout
CI / Format (ruff format) (push) Failing after 27s
CI / Lint (ruff check) (push) Successful in 28s
CI / Sync project version with tag (push) Has been skipped
CI / Type check (ty) (push) Successful in 24s
CI / Tests (push) Successful in 1m11s

decode_secondaries() applied inv_log_transform() to the model's raw
predicted log_mass directly. Since that value isn't itself the output
of log_transform, exp(log_mass) can undershoot _EPS, making
inv_log_transform(log_mass) = exp(log_mass) - _EPS go slightly
negative. Once that secondary spawns a track and its mass is fed back
in as conditioning for a further rollout step, log_transform(mass)
computes log(mass + eps) with mass <= -eps, producing a non-finite
value and raising.

Clip log_mass to log(_EPS) before inverting so the resulting mass is
guaranteed >= 0 (matching the invariant the surrounding comment
already assumed, but didn't enforce).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:08:26 +02:00
parent 057d637080
commit d0381728ee
2 changed files with 36 additions and 3 deletions
+10 -3
View File
@@ -629,9 +629,16 @@ def decode_secondaries(
pre_dir[valid], dir_local[valid, i]
)
# mass is non-negative by construction (inv_log_transform of a real
# number is always > 0); clip to 0 for padded/invalid slots rather than
# leaving a spurious small positive floor from the log inverse.
# 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)
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)
+26
View File
@@ -540,3 +540,29 @@ def test_decode_secondaries_mass_charge_round_trip_with_normalizer():
)
assert sec_mass[0, 0] == pytest.approx(938.27208943, abs=1e-2)
assert sec_charge[0, 0] == pytest.approx(1.0, abs=1e-4)
def test_decode_secondaries_extreme_negative_log_mass_stays_nonnegative():
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] = -50.0 # raw model prediction: extremely negative log_mass
sec_cont[0, 0, 5] = 1.0
_, _, sec_mass, _, _ = decode_secondaries(sec_cont, n_sec, e_sec, pre_dir)
# A raw model prediction isn't itself the output of log_transform, so
# naively applying inv_log_transform can undershoot zero (see
# decode_secondaries) — which then crashes the next log_transform call
# once this mass is fed back in as conditioning during rollout. The
# float32 residual from clipping can land a hair below zero, but must
# stay well above -eps so log_transform(mass) stays finite.
assert sec_mass[0, 0] > -1e-8
log_transform(sec_mass[0, 0])