@@ -207,6 +207,27 @@ def call(self, parameters):
207207 A private method that calls the fitness function with the given parameters and additional keyword arguments.
208208 This method is intended for internal use only.
209209
210+ The NaN/inf guard below protects the **value only, never the gradient**.
211+
212+ A model whose likelihood is NaN or inf is mapped to `resample_figure_of_merit`, so searches that read only
213+ the figure of merit (nested samplers, MCMC) get the resample sentinel and never select the point. Gradient
214+ consumers get no such protection: under `jax.grad`, reverse-mode differentiates *both* branches of an
215+ `xp.where` and multiplies the unselected one by zero, so if the likelihood's derivative is also non-finite
216+ the guard yields `0 * NaN = NaN` and the returned gradient is NaN even though the value looks handled.
217+
218+ This bites only when the masked branch's *derivative* is non-finite — not merely its value. `sqrt(x)` at
219+ x < 0 and `cholesky(A)` for non-positive-definite `A` are NaN in both value and derivative, so they trigger
220+ it; `log(x)` at x < 0 is NaN in value but its derivative `1/x` stays finite, so it does not.
221+
222+ A guard here **cannot** repair this. By the time this method receives `log_likelihood` the non-finite
223+ derivative is already recorded on the autodiff tape, and no transformation of the output can remove it —
224+ an output-side "double-where" does not work. Gradient-safety must be established at the site that creates
225+ the NaN, by never *evaluating* the offending operation at the invalid input.
226+
227+ See autolens_workspace_developer#104, where this was diagnosed, and
228+ `autofit_workspace_test/scripts/jax_assertions/fitness_nan_gradient_contract.py`, which pins the behaviour
229+ described here.
230+
210231 Parameters
211232 ----------
212233 parameters
@@ -235,7 +256,9 @@ def call(self, parameters):
235256 except exc .FitException :
236257 return self .resample_figure_of_merit
237258
238- # Penalize NaNs in the log-likelihood
259+ # Penalize NaNs in the log-likelihood. Value-only: under jax.grad these `where`s still differentiate the
260+ # masked branch, so a non-finite derivative propagates as `0 * NaN = NaN`. See the contract in the
261+ # docstring above -- gradient-safety belongs at the site that creates the NaN, not here.
239262 log_likelihood = self ._xp .where (self ._xp .isnan (log_likelihood ), self .resample_figure_of_merit , log_likelihood )
240263 log_likelihood = self ._xp .where (self ._xp .isinf (log_likelihood ), self .resample_figure_of_merit , log_likelihood )
241264
0 commit comments