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
8 changes: 8 additions & 0 deletions autofit/non_linear/search/mle/bfgs/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@

class AbstractBFGS(AbstractMLE):

# Same contract as ``AbstractMultiStartGradient``: bounds handed to scipy
# change where the fit converges, so the clipper forks the identifier here
# too, re-keying existing (L)BFGS output directories (PyAutoFit#1493).
# ``Drawer`` deliberately does NOT declare this: it inherits the attribute
# from ``AbstractMLE`` but never consumes it, and a setting that cannot
# affect the result must not re-key stored results.
__identifier_fields__ = ("clipper",)

method = None

def __init__(
Expand Down
8 changes: 8 additions & 0 deletions autofit/non_linear/search/mle/multi_start_gradient/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@

class AbstractMultiStartGradient(AbstractMLE):

# The clipper changes where a lane can sit and therefore the result, so two
# runs differing only in clipper must not share an output directory. This
# re-keys every existing multi-start identifier (stored results are orphaned
# on disk, not deleted). Scoped to the clipper-consuming searches only: the
# nested samplers and MCMC searches never touch the clipper and their
# identifiers must stay byte-identical (PyAutoFit#1493).
__identifier_fields__ = ("clipper",)

# Name of the optax update rule, resolved lazily at fit time from ``optax``
# then ``optax.contrib`` (so ``optax`` is only imported when a fit is
# actually run — it is a JAX-only optional dependency). Subclasses set this
Expand Down
104 changes: 104 additions & 0 deletions test_autofit/database/identifier/test_identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,107 @@ def test_dynesty_static():

def test_integer_keys():
assert str(Identifier({1: 1}))


def test_nested_sampler_identifiers_unchanged_by_clipper():
"""
The clipper entering the MLE search identifiers (PyAutoFit#1493) must not
re-key any nested sampler: these hash lists were captured on main before
the change and pin the archived nested-sampling results' directories.
"""
assert Identifier(af.Nautilus()).hash_list == [
"Nautilus",
"n_live",
"3000",
"n_update",
"enlarge_per_dim",
"1.1",
"n_points_min",
"split_threshold",
"100",
"n_networks",
"4",
"n_like_new_bound",
"seed",
"n_shell",
"1",
"n_eff",
"500",
]
assert Identifier(af.DynestyDynamic()).hash_list == [
"DynestyDynamic",
"bound",
"multi",
"sample",
"auto",
"enlarge",
"bootstrap",
"walks",
"5",
"facc",
"0.2",
"slices",
"5",
"fmove",
"0.9",
"max_move",
"100",
]


def test_mcmc_identifiers_unchanged_by_clipper():
assert Identifier(af.Emcee()).hash_list == [
"Emcee",
"nwalkers",
"50",
]
assert Identifier(af.Zeus()).hash_list == [
"Zeus",
"nwalkers",
"50",
"tune",
"True",
"tolerance",
"0.05",
"patience",
"5",
"mu",
"1.0",
"light_mode",
"False",
]


def test_nested_samplers_have_no_clipper():
"""
Tripwire for the hard constraint of PyAutoFit#1493: the clipper is resolved
on AbstractMLE and must stay there. Hoisting it to NonLinearSearch would put
it within reach of the nested samplers' identifier machinery and silently
re-key the nested-sampling archive; this fails loudly instead.
"""
for search in [af.Nautilus(), af.DynestyStatic(), af.DynestyDynamic()]:
assert not hasattr(search, "clipper")


@pytest.mark.parametrize("cls", [af.MultiStartAdam, af.LBFGS])
def test_clipper_forks_mle_identifier(cls):
default = Identifier(cls())
assert Identifier(cls(clipper=af.ClipperNone())) == default
box = Identifier(cls(clipper=af.ClipperPriorBox()))
assert box != default
assert Identifier(cls(clipper=af.ClipperPriorBox(margin=1.0e-3))) != box


def test_drawer_identifier_ignores_clipper():
"""
Drawer inherits the clipper attribute from AbstractMLE but never consumes
it, so a setting that cannot affect its result must not re-key it.
"""
assert Identifier(af.Drawer()).hash_list == [
"Drawer",
"total_draws",
"50",
]
assert Identifier(af.Drawer(clipper=af.ClipperPriorBox())) == Identifier(
af.Drawer()
)
Loading