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
14 changes: 13 additions & 1 deletion autofit/messages/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,19 @@ def log_partition(self, xp=np) -> np.ndarray:
@classmethod
def natural_logpdf(cls, eta, t, log_base, log_partition, xp=np):
eta_t = xp.multiply(eta, t).sum(0)
return xp.nan_to_num(log_base + eta_t - log_partition, nan=-xp.inf)
# Only NaN is replaced. ``nan=-inf`` maps an out-of-support NaN (e.g.
# ``log`` of a negative value under a transformed message) to zero
# density, which is intended -- but ``nan_to_num``'s DEFAULT ``neginf``
# replaces a genuine ``-inf`` with ``-1.8e308``, turning a zero-density
# point into a finite one. That is the opposite of the intent, and it
# matters: ``isfinite`` is what ``optax.apply_if_finite`` and
# ``non_linear.clipper`` branch on to detect a lane leaving the support.
return xp.nan_to_num(
log_base + eta_t - log_partition,
nan=-xp.inf,
neginf=-xp.inf,
posinf=xp.inf,
)

def numerical_logpdf_gradient(
self, x: np.ndarray, eps: float = 1e-6
Expand Down
77 changes: 77 additions & 0 deletions test_autofit/messages/test_natural_logpdf_neginf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
``AbstractMessage.natural_logpdf`` must not clamp a genuine ``-inf``.

The reduction maps NaN to ``-inf`` on purpose -- an out-of-support NaN (``log`` of a
negative value under a transformed message) is zero density. But ``nan_to_num``'s
DEFAULT ``neginf`` replaces a real ``-inf`` with ``-1.7976931348623157e+308``, so the
call did the opposite of its intent for exactly the inputs that were already correct.

``-1.8e308`` is finite, and ``isfinite`` is what ``optax.apply_if_finite`` and
``autofit.non_linear.clipper`` branch on to decide a lane has left the prior support.
"""

import sys

import numpy as np
import pytest

import autofit as af


@pytest.fixture(name="log_gaussian")
def make_log_gaussian():
return af.LogGaussianPrior(mean=0.4, sigma=1.3)


def test__genuine_neginf_survives_the_reduction(log_gaussian):
"""
``log(0) = -inf`` reaches the reduction as ``-inf``; it must come back as ``-inf``,
not as negative float max.
"""
value = log_gaussian.message.logpdf(np.array(0.0))

assert value == -np.inf
assert not np.isfinite(value)
assert value != -sys.float_info.max


def test__nan_still_maps_to_neginf(log_gaussian):
"""
The other half of the same call, which was always correct: ``log(-1) = NaN``, and an
out-of-support NaN is zero density. Pinned so a fix to one half cannot break the other.
"""
value = log_gaussian.message.logpdf(np.array(-1.0))

assert value == -np.inf
assert not np.isnan(value)


@pytest.mark.parametrize("value", [1e-9, 0.001, 0.1, 0.5, 1.0, 2.0, 10.0, 1000.0])
def test__in_support_values_are_unchanged(log_gaussian, value):
"""
The fix must touch only the non-finite branch. Every in-support point is finite before
and after, and the density is the one the prior reports.
"""
assert np.isfinite(log_gaussian.message.logpdf(np.array(value)))


@pytest.mark.parametrize(
"prior, outside",
[
(af.UniformPrior(lower_limit=0.0, upper_limit=2.0), [-1.0, 3.0]),
(af.LogUniformPrior(lower_limit=0.01, upper_limit=100.0), [-1.0, 0.0, 1000.0]),
(af.LogGaussianPrior(mean=0.4, sigma=1.3), [-1.0, 0.0]),
],
ids=["Uniform", "LogUniform", "LogGaussian"],
)
def test__no_prior_family_reports_a_finite_density_off_its_support(prior, outside):
"""
The general form. A finite log density outside the support is a licence for any
``isfinite`` consumer to treat an impossible point as merely a bad one.

``TruncatedGaussianPrior`` is deliberately absent: its message returns finite values
outside its limits for an unrelated reason (``TruncatedNormalMessage`` does not gate on
them), which this fix does not address and must not be read as covering.
"""
for value in outside:
assert prior.message.logpdf(np.array(value)) == -np.inf
Loading