diff --git a/autofit/mapper/prior/log_uniform.py b/autofit/mapper/prior/log_uniform.py index 4958bcd5e..7d0725468 100644 --- a/autofit/mapper/prior/log_uniform.py +++ b/autofit/mapper/prior/log_uniform.py @@ -123,10 +123,21 @@ def log_prior_from_value(self, value, xp=np): used by ``UniformPrior.log_prior_from_value`` which drops ``-log(b - a)`` to return ``0.0``. - Out-of-support (``value`` outside ``[lower_limit, upper_limit]``) returns - ``-inf`` on the JAX path; the NumPy path returns ``-log(value)`` without - bounds-checking to mirror the existing ``UniformPrior`` NumPy semantics - which trust the search to stay inside bounds. + Non-positive ``value`` (``value <= 0``) returns ``-inf``. Emcee's stretch + move proposes physical values that can leave the support and go + non-positive; ``-log`` of a non-positive value is ``NaN``, which propagates + into the summed figure-of-merit and crashes the search with + ``ValueError: Probability function returned NaN``. Returning ``-inf`` + (zero density -> rejected move) keeps the figure-of-merit finite. The + "double where" pattern (a safe surrogate inside the ``log``) ensures no + ``log`` of a non-positive value is evaluated, avoiding NumPy + ``RuntimeWarning``s. + + The NumPy path is otherwise unnormalised and unbounded: for any positive + ``value`` it returns ``-log(value)`` regardless of ``[lower_limit, + upper_limit]`` (dropping the normalisation constant, matching + ``UniformPrior``'s convention of returning ``0.0``). The JAX path + additionally returns ``-inf`` outside ``[lower_limit, upper_limit]``. Parameters ---------- @@ -137,7 +148,8 @@ def log_prior_from_value(self, value, xp=np): Array-module to dispatch on (``numpy`` or ``jax.numpy``). Default ``numpy``. """ if xp is np: - return -np.log(value) + positive = value > 0.0 + return xp.where(positive, -xp.log(xp.where(positive, value, 1.0)), -xp.inf) in_bounds = (value >= self.lower_limit) & (value <= self.upper_limit) return xp.where(in_bounds, -xp.log(value), -xp.inf) diff --git a/test_autofit/mapper/prior/test_prior.py b/test_autofit/mapper/prior/test_prior.py index a2b4895a9..730a95073 100644 --- a/test_autofit/mapper/prior/test_prior.py +++ b/test_autofit/mapper/prior/test_prior.py @@ -1,4 +1,5 @@ import math +import warnings import numpy as np import pytest @@ -216,6 +217,30 @@ def test__log_prior_from_value(self): -np.log(4.0), 1.0e-12 ) + def test__log_prior_from_value__non_positive_returns_neg_inf(self): + # Regression (PyAutoHeart #27 / release run 28784914443): Emcee's stretch + # move proposes physical values that can go non-positive for a LogUniform + # parameter. `-log(value)` of a non-positive value is NaN, which propagated + # into the summed figure-of-merit and crashed the search with + # "ValueError: Probability function returned NaN". Non-positive values must + # return -inf (zero density -> the move is rejected), and evaluating the + # log-prior must not emit a NumPy "invalid value in log" RuntimeWarning. + log_uniform = af.LogUniformPrior(lower_limit=1e-3, upper_limit=1e3) + + with warnings.catch_warnings(): + warnings.simplefilter("error", category=RuntimeWarning) + assert log_uniform.log_prior_from_value(value=-1.0) == float("-inf") + assert log_uniform.log_prior_from_value(value=0.0) == float("-inf") + + # Positive values are unchanged: the NumPy path stays unnormalised and + # unbounded, returning -log(value) regardless of (lower_limit, upper_limit). + assert log_uniform.log_prior_from_value(value=10.0) == pytest.approx( + -np.log(10.0), 1.0e-12 + ) + assert log_uniform.log_prior_from_value(value=1.0e4) == pytest.approx( + -np.log(1.0e4), 1.0e-12 + ) + def test__lower_limit_zero_or_below_raises_error(self): with pytest.raises(exc.PriorException): af.LogUniformPrior(lower_limit=-1.0, upper_limit=1.0)