diff --git a/autofit/non_linear/samples/interface.py b/autofit/non_linear/samples/interface.py index 36b282c93..d2aad2abd 100644 --- a/autofit/non_linear/samples/interface.py +++ b/autofit/non_linear/samples/interface.py @@ -53,6 +53,13 @@ def __init__(self, model: AbstractPriorModel): self._names = None self._instance = None + def _rebind_model(self, model: AbstractPriorModel) -> None: + """Rebind a copied samples object without retaining model-derived caches.""" + self.model = model + self._paths = None + self._names = None + self._instance = None + @property def instance(self): if self._instance is None: diff --git a/autofit/non_linear/samples/samples.py b/autofit/non_linear/samples/samples.py index cda092f27..f09be3800 100644 --- a/autofit/non_linear/samples/samples.py +++ b/autofit/non_linear/samples/samples.py @@ -500,7 +500,7 @@ def with_paths(self, paths: Union[List[Tuple[str, ...]], List[str]]) -> "Samples A set of samples with a reduced set of attributes """ with_paths = copy(self) - with_paths.model = self.model.with_paths(paths) + with_paths._rebind_model(model=self.model.with_paths(paths)) with_paths.sample_list = [ sample.with_paths(paths) for sample in self.sample_list ] @@ -526,7 +526,7 @@ def without_paths( A set of samples with a reduced set of attributes """ with_paths = copy(self) - with_paths.model = self.model.without_paths(paths) + with_paths._rebind_model(model=self.model.without_paths(paths)) with_paths.sample_list = [ sample.without_paths(paths) for sample in self.sample_list ] @@ -538,9 +538,7 @@ def subsamples(self, model): path_map = self.path_map_for_model(model) copied = copy(self) - copied._paths = None - copied._names = None - copied.model = model + copied._rebind_model(model=model) copied.sample_list = [sample.subsample(path_map) for sample in self.sample_list] return copied diff --git a/autofit/non_linear/samples/summary.py b/autofit/non_linear/samples/summary.py index 920868549..cb6dcd5e1 100644 --- a/autofit/non_linear/samples/summary.py +++ b/autofit/non_linear/samples/summary.py @@ -77,9 +77,7 @@ def subsamples(self, model): return None copied = copy(self) - copied._paths = None - copied._names = None - copied.model = model + copied._rebind_model(model=model) copied._max_log_likelihood_sample = self.max_log_likelihood_sample.subsample( self.path_map_for_model(model) diff --git a/autofit/non_linear/search/abstract_search.py b/autofit/non_linear/search/abstract_search.py index 8b1fcf29e..54237cfbc 100644 --- a/autofit/non_linear/search/abstract_search.py +++ b/autofit/non_linear/search/abstract_search.py @@ -756,7 +756,7 @@ def start_resume_fit(self, analysis: Analysis, model: AbstractPriorModel) -> Res if mode == 1: try: - samples_summary.instance + samples_summary.max_log_likelihood() except exc.FitException as error: samples = self._test_mode_samples_after_rejected_fit( samples=samples, diff --git a/test_autofit/non_linear/samples/test_samples.py b/test_autofit/non_linear/samples/test_samples.py index f3e43cff2..cf85d69c9 100644 --- a/test_autofit/non_linear/samples/test_samples.py +++ b/test_autofit/non_linear/samples/test_samples.py @@ -29,6 +29,62 @@ def _guarded_samples(parameter_lists, log_likelihood_list, weight_list): ) +def _factor_graph_samples(): + shared_value = af.UniformPrior(lower_limit=0.0, upper_limit=1.0) + factor_models = [ + af.Collection(galaxies=af.Model(_RejectsLowStoredValue, value=shared_value)) + for _ in range(2) + ] + factor_graph = af.FactorGraphModel( + *[ + af.AnalysisFactor( + prior_model=factor_model, + analysis=af.m.MockAnalysis(), + ) + for factor_model in factor_models + ] + ) + model = factor_graph.global_prior_model + samples = af.SamplesPDF( + model=model, + sample_list=af.Sample.from_lists( + model=model, + parameter_lists=[[0.9]], + log_likelihood_list=[1.0], + log_prior_list=[0.0], + weight_list=[1.0], + ), + ) + return samples, factor_models[0] + + +@pytest.mark.parametrize("use_summary", [False, True]) +def test__subsamples__rebuilds_instance_after_model_rebind(use_summary): + samples, factor_model = _factor_graph_samples() + source = samples.summary() if use_summary else samples + + global_instance = source.instance + child = source.subsamples(model=factor_model) + + assert child._instance is None + assert child.instance is not global_instance + assert child.instance.galaxies.value == pytest.approx(0.9) + + +@pytest.mark.parametrize("method_name", ["with_paths", "without_paths"]) +def test__path_filter__clears_model_derived_caches(samples_x5, method_name): + samples_x5.instance + samples_x5.paths + samples_x5.names + + paths = [("mock_class_1", "one")] + filtered = getattr(samples_x5, method_name)(paths) + + assert filtered._instance is None + assert filtered._paths is None + assert filtered._names is None + + def test__table__headers(samples_x5): assert samples_x5._headers == [ "mock_class_1.one", diff --git a/test_autofit/non_linear/search/test_abstract_search.py b/test_autofit/non_linear/search/test_abstract_search.py index 3e4765662..1dd42d239 100644 --- a/test_autofit/non_linear/search/test_abstract_search.py +++ b/test_autofit/non_linear/search/test_abstract_search.py @@ -596,6 +596,50 @@ def test__test_mode_1__factor_graph_children_and_sample_type_are_preserved( assert len(result) == 2 assert all(child.instance.galaxies.value >= 0.75 for child in result) + def test__test_mode_1__valid_factor_graph_children_keep_per_analysis_model( + self, monkeypatch + ): + """Validation must not leave child results bound to a cached global instance.""" + monkeypatch.setenv("PYAUTO_TEST_MODE", "1") + + shared_value = af.UniformPrior(lower_limit=0.0, upper_limit=1.0) + factor_models = [ + af.Collection(galaxies=af.Model(_RejectsLowValue, value=shared_value)) + for _ in range(2) + ] + factor_graph = af.FactorGraphModel( + *[ + af.AnalysisFactor( + prior_model=factor_model, + analysis=af.m.MockAnalysis(), + ) + for factor_model in factor_models + ] + ) + model = factor_graph.global_prior_model + valid_samples = _TaggedSamplesPDF( + model=model, + sample_list=af.Sample.from_lists( + model=model, + parameter_lists=[[0.9]], + log_likelihood_list=[1.0], + log_prior_list=[0.0], + weight_list=[1.0], + ), + samples_info={"log_evidence": 1.0, "sampler_marker": "retained"}, + ) + + result = _RejectedFinalSampleSearch(samples=valid_samples).fit( + model=model, + analysis=factor_graph, + ) + + assert type(result.samples) is _TaggedSamplesPDF + assert len(result) == 2 + assert all( + child.instance.galaxies.value == pytest.approx(0.9) for child in result + ) + def test__normal_mode__fitexception_still_propagates(self, monkeypatch): monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False) model, rejected_samples = _model_and_rejected_samples(_RejectsLowValue)