Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions autofit/mapper/prior/log_uniform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
----------
Expand All @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions test_autofit/mapper/prior/test_prior.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
import warnings

import numpy as np
import pytest
Expand Down Expand Up @@ -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)
Expand Down
Loading