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
97 changes: 96 additions & 1 deletion autofit/non_linear/search/abstract_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@
#: a hand-set ``1e100`` in a workspace config reads as "never" too.
ITERATIONS_NEVER = 1e90

# A reduced test-mode fit needs a valid representative instance for result
# construction and search chaining. Keep fallback sampling bounded so an
# impossible model fails clearly instead of hanging a smoke-test worker.
TEST_MODE_REPRESENTATIVE_MAX_ATTEMPTS = 100


def check_cores(func):
"""
Expand Down Expand Up @@ -747,8 +752,22 @@ def start_resume_fit(self, analysis: Analysis, model: AbstractPriorModel) -> Res
during_analysis=False,
)

samples_summary = samples.summary()

if mode == 1:
try:
samples_summary.instance
except exc.FitException as error:
samples = self._test_mode_samples_after_rejected_fit(
model=model,
error=error,
)
samples_summary = samples.summary()
self.paths.save_samples_summary(samples_summary=samples_summary)
self.paths.save_samples(samples=samples)

result = analysis.make_result(
samples_summary=samples.summary(),
samples_summary=samples_summary,
paths=self.paths,
samples=samples,
search_internal=search_internal,
Expand All @@ -763,6 +782,82 @@ def start_resume_fit(self, analysis: Analysis, model: AbstractPriorModel) -> Res

return result

def _test_mode_samples_after_rejected_fit(
self,
model: AbstractPriorModel,
error: exc.FitException,
) -> Samples:
"""Build valid representative samples after a mode-1 rejected result.

``Fitness`` maps :class:`FitException` to the sampler's rejection
sentinel. A production search naturally moves on to another point,
but ``PYAUTO_TEST_MODE=1`` may stop after that first evaluation. Its
posterior can therefore contain only the rejected point, which cannot
be reconstructed while finalizing the result.

Try the prior medians first, then a deterministic sequence of prior
draws. Every synthetic sample is validated before it is returned so
result construction and downstream chaining cannot reconstruct another
rejected point. The fixed seed keeps smoke tests reproducible without
changing the application's global random state.
"""
from autofit.non_linear.samples.pdf import SamplesPDF

logger.warning(
"TEST MODE 1: the reduced search's final sample raised "
f"FitException ({error.__cause__ or error!r}); replacing it with "
"a valid representative sample for result construction."
)

rng = np.random.default_rng(seed=0)
last_error = error

for attempt in range(TEST_MODE_REPRESENTATIVE_MAX_ATTEMPTS):
unit_vector = (
[0.5] * model.prior_count
if attempt == 0
else rng.random(model.prior_count).tolist()
)

try:
parameter_vector = [
float(value)
for value in model.vector_from_unit_vector(
unit_vector=unit_vector,
)
]
sample_list = self._build_fake_samples(
model=model,
parameter_vector=parameter_vector,
log_likelihood=-1.0e99,
)

for sample in sample_list:
model.instance_from_vector(
vector=sample.parameter_lists_for_model(model)
)
except exc.FitException as candidate_error:
last_error = candidate_error
continue

samples_info = {
"total_iterations": 1,
"time": 0.0,
"log_evidence": -1.0e99,
}
samples_info.update(self._test_mode_samples_info())

return SamplesPDF(
model=model,
sample_list=sample_list,
samples_info=samples_info,
)

raise exc.FitException(
"TEST MODE 1 could not construct a valid representative result "
f"after {TEST_MODE_REPRESENTATIVE_MAX_ATTEMPTS} attempts."
) from last_error

def result_via_completed_fit(
self,
analysis: Analysis,
Expand Down
118 changes: 118 additions & 0 deletions test_autofit/non_linear/search/test_abstract_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import autofit as af
from autonerves import conf
from autofit.non_linear.paths.null import NullPaths

pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning")

Expand Down Expand Up @@ -473,3 +474,120 @@ def log_likelihood_function(self, instance):

with pytest.raises(ValueError):
search.fit(model=af.Model(af.m.MockClassx2), analysis=_BrokenAnalysis())


class _RejectsLowValue:
def __init__(self, value):
if value < 0.75:
raise af.exc.FitException("value must be at least 0.75")
self.value = value


class _RejectsLowValueUnexpectedly:
def __init__(self, value):
if value < 0.75:
raise ValueError("unexpected constructor failure")
self.value = value


class _AlwaysRejects:
def __init__(self, value):
raise af.exc.FitException("no valid instance exists")


class _RejectedFinalSampleSearch(af.mock.MockSearch):
"""Search double whose reduced sampler returns one rejected region."""

def __init__(self, samples):
super().__init__(fit_fast=False, paths=NullPaths())
self._rejected_samples = samples

def _fit(self, model, analysis):
return object(), object()

def perform_update(
self,
model,
analysis,
during_analysis,
fitness=None,
search_internal=None,
):
return self._rejected_samples


def _model_and_rejected_samples(cls):
model = af.Model(cls)
model.value = af.UniformPrior(lower_limit=0.0, upper_limit=1.0)
sample_list = af.DynestyStatic._build_fake_samples(
model=model,
parameter_vector=[0.1],
log_likelihood=-1.0e99,
)
samples = af.SamplesPDF(
model=model,
sample_list=sample_list,
samples_info={"log_evidence": -1.0e99},
)
return model, samples


class TestReducedModeRejectedFinalSample:
def test__test_mode_1__fitexception_gets_valid_representative(self, monkeypatch):
monkeypatch.setenv("PYAUTO_TEST_MODE", "1")
model, rejected_samples = _model_and_rejected_samples(_RejectsLowValue)

result = _RejectedFinalSampleSearch(samples=rejected_samples).fit(
model=model,
analysis=af.m.MockAnalysis(),
)

assert result.samples_summary.instance.value >= 0.75
assert result.samples.max_log_likelihood_sample.log_likelihood == pytest.approx(
-1.0e99
)
assert all(instance.value >= 0.75 for instance in result.samples.instances)

def test__normal_mode__fitexception_still_propagates(self, monkeypatch):
monkeypatch.delenv("PYAUTO_TEST_MODE", raising=False)
model, rejected_samples = _model_and_rejected_samples(_RejectsLowValue)

result = _RejectedFinalSampleSearch(samples=rejected_samples).fit(
model=model,
analysis=af.m.MockAnalysis(),
)

with pytest.raises(af.exc.FitException):
result.samples_summary.instance

def test__test_mode_1__non_fitexception_still_propagates(self, monkeypatch):
monkeypatch.setenv("PYAUTO_TEST_MODE", "1")
model, rejected_samples = _model_and_rejected_samples(
_RejectsLowValueUnexpectedly
)

with pytest.raises(ValueError, match="unexpected constructor failure"):
_RejectedFinalSampleSearch(samples=rejected_samples).fit(
model=model,
analysis=af.m.MockAnalysis(),
)

def test__test_mode_1__bounded_failure_is_clear(self, monkeypatch):
from autofit.non_linear.search import abstract_search

monkeypatch.setenv("PYAUTO_TEST_MODE", "1")
monkeypatch.setattr(
abstract_search,
"TEST_MODE_REPRESENTATIVE_MAX_ATTEMPTS",
2,
)
model, rejected_samples = _model_and_rejected_samples(_AlwaysRejects)

with pytest.raises(
af.exc.FitException,
match="could not construct a valid representative result after 2 attempts",
):
_RejectedFinalSampleSearch(samples=rejected_samples).fit(
model=model,
analysis=af.m.MockAnalysis(),
)
Loading