docs: pin Fitness's NaN guard contract — value-only, never gradient#1392
Merged
Conversation
The xp.where NaN/inf guard in Fitness.call repairs the VALUE but not the gradient: under jax.grad, reverse-mode differentiates the masked branch and computes 0 * NaN = NaN whenever the likelihood's derivative is also non-finite. Searches reading only the figure of merit are protected; gradient consumers are not. A guard here cannot fix this — by the time call() receives log_likelihood the non-finite derivative is already on the tape, so an output-side double-where is measurably ineffective. Gradient-safety belongs at the site that creates the NaN. Docstring + comment only; no behaviour change. Diagnosed in autolens_workspace_developer#104; pinned by autofit_workspace_test/scripts/jax_assertions/fitness_nan_gradient_contract.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1391.
Documents what
Fitness's NaN/inf resample guard actually promises. Docstring and comment only — no behaviour change.The problem
Fitness.callguards a bad likelihood withxp.where(xp.isnan(ll), resample_figure_of_merit, ll). It repairs the value: searches that read only the figure of merit get the resample sentinel and never select the point. Gradient consumers get nothing — underjax.grad, reverse-mode differentiates both branches of awhereand multiplies the unselected one by zero, so0 * NaN = NaN. The value looks handled; the gradient is poison.This was diagnosed in autolens_workspace_developer#104 (the pixelized likelihood's NaN walls) and retroactively explains two #100/#101 mysteries: trajectories died with a non-finite objective rather than a NaN one, and
optax.apply_if_finitelatched at the cliff rather than stepping past it.Why this is a contract, not a fix
The obvious remedy does not work, and that is the point of this PR. Measured:
By the time
callreceiveslog_likelihood, the non-finite derivative is already on the autodiff tape; no transformation of the output removes it. Only refusing to evaluate the offending operation at the invalid input works — and that requires knowing which input is invalid, which is knowable at the NaN's source, not at theFitnessboundary where the likelihood is an opaque already-computed scalar.So
Fitness.callis structurally incapable of fixing this. Rather than add a guard that cannot deliver, this PR documents the true contract and points at where gradient-safety must be established. The real fix for the pixelized case is the autoarray Cholesky (a separate issue).The trap is narrower than it first looked
It fires only when the masked branch's derivative is non-finite, not merely its value:
log(x), x<01/x= finite0 * -1 = 0sqrt(x), x<0cholesky(A), non-PDAn earlier write-up of this claimed the guard "does not protect gradient consumers at all". That was overreach and is corrected here and in the #104 findings doc.
API Changes
None. Docstring and comment only; no signature, behaviour or public-surface change. Downstream workspaces need no migration.
Detail
Fitness.callis byte-for-byte equivalent at runtimeTesting
pytest test_autofit/→ 1499 passed, 1 skipped (no delta, as expected for a docstring-only change).autofit_workspace_test/scripts/jax_assertions/fitness_nan_gradient_contract.py(companion PR), which asserts: the value is guarded;jax.gradis NaN; the output-side double-wheredoes not fix it; input-side safe-x does. JAX-only, so it lives there rather than intest_autofit/(unit tests are numpy-only per repo policy). Runs on CPU in seconds.Gate
PyAutoLens: 1 uncommitted source change(s)(a straypaper_jax/paper.mdedit in the main checkout, untouched by this branch).