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
16 changes: 15 additions & 1 deletion autofit/non_linear/search/abstract_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,21 @@ def output_search_internal(self, search_internal):

@property
def _updater(self):
if not hasattr(self, "_search_updater") or self._search_updater is None:
# The cached ``SearchUpdater`` must be invalidated whenever
# ``self.paths`` is reassigned to a new object — otherwise the
# updater holds a stale reference to the old paths and writes
# output (samples, visualizations, profiles) under the wrong
# directory. This happens routinely when a single search
# instance is reused across factor optimisations in the EP
# loop: ``AbstractSearch.optimise(factor_approx)`` mutates
# ``self.paths = SubDirectoryPaths(...)`` per factor and per EP
# iteration, but the updater would otherwise stay pinned to
# whichever paths were live the first time ``_updater`` was
# accessed. Identity comparison (``is not``) is the right test:
# the search instance receives a freshly-constructed
# ``SubDirectoryPaths`` each time, never an in-place mutation.
cached = getattr(self, "_search_updater", None)
if cached is None or cached._paths is not self.paths:
from autofit.non_linear.search.updater import SearchUpdater

self._search_updater = SearchUpdater(
Expand Down
27 changes: 27 additions & 0 deletions test_autofit/non_linear/search/test_abstract_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,33 @@ def test__identifier_fields_differ_across_searches(self):
assert "nlive" in dynesty.__identifier_fields__


class TestUpdaterPathsRefresh:
"""
The cached ``SearchUpdater`` must be invalidated when ``self.paths``
is reassigned to a new object — otherwise output (samples, viz,
profiling) keeps writing under the FIRST paths the search ever saw.
The EP loop does this routinely:
``AbstractSearch.optimise(factor_approx)`` reassigns ``self.paths``
to a fresh ``SubDirectoryPaths`` per factor and per EP iteration.
"""

def test__updater_refreshes_when_paths_reassigned(self):
search = af.DynestyStatic(name="updater_paths_test_a")
first_updater = search._updater
first_paths = search.paths
# Sanity: cache hit on second access with no path change.
assert search._updater is first_updater

search.paths = af.DirectoryPaths(name="updater_paths_test_b")
second_updater = search._updater

assert second_updater is not first_updater, (
"Expected a fresh SearchUpdater after self.paths was reassigned"
)
assert second_updater._paths is search.paths
assert second_updater._paths is not first_paths


class TestLabels:
def test_param_names(self):
model = af.Model(af.m.MockClassx4)
Expand Down
Loading