diff --git a/autofit/non_linear/mock/mock_samples.py b/autofit/non_linear/mock/mock_samples.py index 14f89fe85..6ce8abf85 100644 --- a/autofit/non_linear/mock/mock_samples.py +++ b/autofit/non_linear/mock/mock_samples.py @@ -8,6 +8,22 @@ def samples_with_log_likelihood_list(log_likelihood_list): ] +def prior_median_kwargs(model): + """ + Placeholder sample values for a model, taken as the median of each prior. + + A blanket value like ``1.0`` is invalid for any parameter whose prior + constrains it, so filling a mock sample with one builds an unphysical + instance. For example an elliptical profile's ``ell_comps`` must have a + magnitude below 1, and ``(1.0, 1.0)`` raises on construction. Prior medians + are always in range, so they are safe for every model. + """ + if not model: + return {} + + return {path: prior.value_for(0.5) for path, prior in model.path_priors_tuples} + + class MockSamples(SamplesPDF): def __init__( self, @@ -40,7 +56,7 @@ def default_sample_list(self): log_likelihood=log_likelihood, log_prior=0.0, weight=0.0, - kwargs={path: 1.0 for path in self.model.paths} if self.model else {}, + kwargs=prior_median_kwargs(self.model), ) for log_likelihood in range(3) ] diff --git a/autofit/non_linear/mock/mock_samples_summary.py b/autofit/non_linear/mock/mock_samples_summary.py index 509c9c493..a336b4d3a 100644 --- a/autofit/non_linear/mock/mock_samples_summary.py +++ b/autofit/non_linear/mock/mock_samples_summary.py @@ -1,4 +1,5 @@ from autofit.mapper.prior_model.collection import Collection +from autofit.non_linear.mock.mock_samples import prior_median_kwargs from autofit.non_linear.samples.sample import Sample from autofit.non_linear.samples.summary import SamplesSummary @@ -23,7 +24,7 @@ def __init__( self._max_log_likelihood_instance = max_log_likelihood_instance self._prior_means = prior_means - self._kwargs = {path: 1.0 for path in self.model.paths} if self.model else {} + self._kwargs = prior_median_kwargs(self.model) @property def max_log_likelihood_sample(self): diff --git a/autofit/non_linear/mock/mock_search.py b/autofit/non_linear/mock/mock_search.py index dec1efd22..2a590b219 100644 --- a/autofit/non_linear/mock/mock_search.py +++ b/autofit/non_linear/mock/mock_search.py @@ -3,7 +3,7 @@ from autofit import exc from autofit.graphical import FactorApproximation from autofit.graphical.utils import Status -from autofit.non_linear.mock.mock_samples import MockSamples +from autofit.non_linear.mock.mock_samples import MockSamples, prior_median_kwargs from autofit.non_linear.search.abstract_search import NonLinearSearch from autofit.non_linear.mock.mock_result import MockResult from autofit.non_linear.mock.mock_samples_summary import MockSamplesSummary @@ -25,7 +25,7 @@ def samples_with_log_likelihood_list(log_likelihood_list, kwargs): def _make_samples(model): - return {path: prior.value_for(0.5) for path, prior in model.path_priors_tuples} + return prior_median_kwargs(model) class MockSearch(NonLinearSearch): diff --git a/test_autofit/non_linear/samples/test_mock_placeholders.py b/test_autofit/non_linear/samples/test_mock_placeholders.py new file mode 100644 index 000000000..521ee954b --- /dev/null +++ b/test_autofit/non_linear/samples/test_mock_placeholders.py @@ -0,0 +1,63 @@ +import pytest + +import autofit as af + + +class _RejectsUnitMagnitude: + """ + Mirrors an elliptical profile's ``ell_comps`` guard: the pair is only + meaningful while its magnitude is below 1. + """ + + def __init__(self, ell_comps=(0.0, 0.0)): + if ell_comps[0] ** 2 + ell_comps[1] ** 2 >= 1.0: + raise ValueError(f"ell_comps magnitude is not below 1: {ell_comps}") + + self.ell_comps = ell_comps + + +def _guarded_model(): + model = af.Model(_RejectsUnitMagnitude) + model.ell_comps.ell_comps_0 = af.UniformPrior(lower_limit=0.0, upper_limit=0.5) + model.ell_comps.ell_comps_1 = af.UniformPrior(lower_limit=0.0, upper_limit=0.5) + return model + + +def test__mock_samples_summary__fills_with_prior_medians(): + summary = af.m.MockSamplesSummary(model=_guarded_model()) + + assert list(summary.max_log_likelihood_sample.kwargs.values()) == [0.25, 0.25] + assert list(summary.median_pdf_sample.kwargs.values()) == [0.25, 0.25] + + +def test__mock_samples__default_sample_list_fills_with_prior_medians(): + samples = af.m.MockSamples(model=_guarded_model()) + + for sample in samples.sample_list: + assert list(sample.kwargs.values()) == [0.25, 0.25] + + +def test__mock_result_without_samples_summary__builds_a_valid_instance(): + """ + A ``MockResult`` given a model but no ``samples_summary`` builds its own. + That placeholder used to fill every parameter with ``1.0``, which is an + invalid ``ell_comps`` — so reading ``.instance`` raised. + """ + instance = af.m.MockResult(model=_guarded_model()).instance + + assert instance.ell_comps == (0.25, 0.25) + + +def test__all_ones_placeholder_would_be_rejected(): + """ + Guards the test above: confirms the model really does reject the old + blanket ``1.0`` fill, so it cannot silently stop testing anything. + """ + model = _guarded_model() + + assert model.instance_from_prior_medians().ell_comps == (0.25, 0.25) + + with pytest.raises(ValueError, match="magnitude is not below 1"): + model.instance_for_arguments( + {prior: 1.0 for prior in model.priors_ordered_by_id} + )