|
1 | 1 | import pickle |
| 2 | +from copy import copy |
2 | 3 |
|
| 4 | +import numpy as np |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | import autofit as af |
@@ -30,3 +32,129 @@ def test_pickle(log_gaussian): |
30 | 32 |
|
31 | 33 | def test_identifier(log_gaussian): |
32 | 34 | Identifier(log_gaussian) |
| 35 | + |
| 36 | + |
| 37 | +# === PyAutoFit#1526: the prior declares its own (0, inf) support === |
| 38 | +# |
| 39 | +# The support was always (0, inf) -- ``log_prior_from_value`` returns -inf for |
| 40 | +# ``value <= 0`` -- but the prior reported (-inf, inf), because ``Prior.__getattr__`` |
| 41 | +# delegated to a ``TransformedMessage`` whose limits default to +/-inf and were never |
| 42 | +# set. ``ClipperPriorBox`` worked around it with an ``isinstance`` switch; every other |
| 43 | +# consumer of ``lower_limit`` was simply told the wrong thing. |
| 44 | +# |
| 45 | +# The pinned values below are the pre-change ones, measured on the commit before the |
| 46 | +# fix. They are the guarantee that declaring the support moved *what the prior says* |
| 47 | +# and nothing about *what it computes*. |
| 48 | + |
| 49 | + |
| 50 | +def test__reports_its_own_support(log_gaussian): |
| 51 | + assert log_gaussian.lower_limit == 0.0 |
| 52 | + assert log_gaussian.upper_limit == float("inf") |
| 53 | + assert log_gaussian.limits == (0.0, float("inf")) |
| 54 | + |
| 55 | + |
| 56 | +def test__lower_bound_is_strict__upper_is_not(log_gaussian): |
| 57 | + """ |
| 58 | + The support is the *open* (0, inf): ``log_prior_from_value(0.0)`` is -inf, so a |
| 59 | + consumer that clips onto the reported bound must know to stay strictly above it. |
| 60 | + """ |
| 61 | + assert log_gaussian.lower_limit_strict is True |
| 62 | + assert log_gaussian.upper_limit_strict is False |
| 63 | + |
| 64 | + assert log_gaussian.log_prior_from_value(log_gaussian.lower_limit) == -np.inf |
| 65 | + |
| 66 | + |
| 67 | +def test__other_prior_families_keep_their_limits(): |
| 68 | + """ |
| 69 | + ``Prior.limits`` now derives from ``lower_limit``/``upper_limit`` rather than |
| 70 | + returning a hardcoded (-inf, inf). GaussianPrior must still be unbounded. |
| 71 | + """ |
| 72 | + assert af.GaussianPrior(mean=0.0, sigma=1.0).limits == (-np.inf, np.inf) |
| 73 | + assert af.UniformPrior(lower_limit=0.0, upper_limit=2.0).limits == (0.0, 2.0) |
| 74 | + assert af.LogUniformPrior(lower_limit=0.01, upper_limit=100.0).limits == ( |
| 75 | + 0.01, |
| 76 | + 100.0, |
| 77 | + ) |
| 78 | + |
| 79 | + for cls in (af.GaussianPrior, af.UniformPrior, af.LogUniformPrior): |
| 80 | + assert cls.lower_limit_strict is False |
| 81 | + assert cls.upper_limit_strict is False |
| 82 | + |
| 83 | + |
| 84 | +@pytest.mark.parametrize( |
| 85 | + "value, expected", |
| 86 | + [ |
| 87 | + (-3.0, -np.inf), |
| 88 | + (-1e-09, -np.inf), |
| 89 | + (0.0, -np.inf), |
| 90 | + (1e-12, -204.83588563011642), |
| 91 | + (0.001, -8.892033838618836), |
| 92 | + (0.1, 0.14164835190717184), |
| 93 | + (0.5, 0.3396055360729164), |
| 94 | + (1.0, -0.04733727810650888), |
| 95 | + (2.0, -0.7185718164978876), |
| 96 | + (10.0, -3.3735407249713125), |
| 97 | + (1000.0, -19.437601069254285), |
| 98 | + ], |
| 99 | +) |
| 100 | +def test__log_prior_from_value_is_unchanged(value, expected): |
| 101 | + """ |
| 102 | + The density was always correct; only the reported limits were wrong. Pinned |
| 103 | + against the pre-change values either side of zero. |
| 104 | + """ |
| 105 | + prior = af.LogGaussianPrior(mean=0.4, sigma=1.3) |
| 106 | + assert prior.log_prior_from_value(value) == pytest.approx(expected, rel=1e-12) |
| 107 | + |
| 108 | + |
| 109 | +@pytest.mark.parametrize( |
| 110 | + "unit, expected", |
| 111 | + [ |
| 112 | + (1e-09, 0.0006129978595719644), |
| 113 | + (0.0001, 0.011858368820420245), |
| 114 | + (0.01, 0.07249394511581292), |
| 115 | + (0.1, 0.2819523947584061), |
| 116 | + (0.25, 0.6207439038545902), |
| 117 | + (0.5, 1.4918246976412703), |
| 118 | + (0.75, 3.5852803622761034), |
| 119 | + (0.9, 7.893321602745905), |
| 120 | + (0.99, 30.69968015862632), |
| 121 | + (0.999999999, 3630.585196715403), |
| 122 | + ], |
| 123 | +) |
| 124 | +def test__unit_cube_mapping_is_unchanged(unit, expected): |
| 125 | + """ |
| 126 | + The nested samplers work in unit-cube coordinates and map through the prior. The |
| 127 | + limits live on the prior while the mapping lives on the message stack, which this |
| 128 | + change does not touch -- so no stored nested-sampling result shifts. |
| 129 | + """ |
| 130 | + prior = af.LogGaussianPrior(mean=0.4, sigma=1.3) |
| 131 | + assert prior.value_for(unit) == pytest.approx(expected, rel=1e-12) |
| 132 | + assert prior.unit_value_for(prior.value_for(unit)) == pytest.approx(unit, rel=1e-6) |
| 133 | + |
| 134 | + |
| 135 | +def test__identifier_is_unchanged(): |
| 136 | + """ |
| 137 | + If the declared limits fed the identifier, every existing output directory would |
| 138 | + re-key and its stored results would be orphaned. ``__identifier_fields__`` is |
| 139 | + ("mean", "sigma"), and the limits are instance/class attributes outside it, so the |
| 140 | + hash is untouched. Pinned to the pre-change value. |
| 141 | + """ |
| 142 | + prior = af.LogGaussianPrior(mean=0.4, sigma=1.3) |
| 143 | + assert str(Identifier(prior)) == "34cb61ade6bafa6050229e8b6b390235" |
| 144 | + |
| 145 | + |
| 146 | +def test__declared_support_survives_copy_pickle_and_projection(log_gaussian): |
| 147 | + """ |
| 148 | + The limits are derived in ``__init__`` rather than carried as parameters, which is |
| 149 | + what makes them survive every path that rebuilds the prior -- including the JAX |
| 150 | + pytree round-trip, where only (mean, sigma, id) are flattened. |
| 151 | + """ |
| 152 | + assert copy(log_gaussian).lower_limit == 0.0 |
| 153 | + assert pickle.loads(pickle.dumps(log_gaussian)).lower_limit == 0.0 |
| 154 | + |
| 155 | + samples = np.exp(np.random.default_rng(0).normal(1.0, 2.0, 500)) |
| 156 | + projected = log_gaussian.project(samples, np.zeros(500)) |
| 157 | + assert projected.lower_limit == 0.0 |
| 158 | + |
| 159 | + rebuilt = af.LogGaussianPrior.tree_unflatten((), log_gaussian.tree_flatten()[0]) |
| 160 | + assert rebuilt.lower_limit == 0.0 |
0 commit comments