diff --git a/autofit/aggregator/base.py b/autofit/aggregator/base.py index 164ed2681..71ae38f94 100644 --- a/autofit/aggregator/base.py +++ b/autofit/aggregator/base.py @@ -1,13 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod from functools import partial -import logging from typing import List, Optional, Generator import autofit as af -logger = logging.getLogger(__name__) - class AggBase(ABC): def __init__(self, aggregator: af.Aggregator): @@ -136,36 +133,7 @@ def func_gen(fit: af.Fit, minimum_weight: float) -> List[object]: @staticmethod def _valid_sample_instance_pairs(samples, minimum_weight: float): - """Return weighted samples whose model instances still reconstruct. - - Constructor validation can become stricter after a result was written. - Such historical points are not usable objects, but they must not make an - entire aggregator query fail. ``FitException`` is the narrow model-point - rejection contract; programming errors continue to propagate. - """ - pairs = [] - rejected = 0 - - for sample in samples.sample_list: - if sample.weight <= minimum_weight: - continue - try: - instance = samples.model.instance_from_vector( - sample.parameter_lists_for_model(model=samples.model) - ) - except af.exc.FitException: - rejected += 1 - continue - pairs.append((sample, instance)) - - if rejected: - logger.warning( - "Skipped %d stored sample(s) rejected by current model " - "validation while building aggregator objects.", - rejected, - ) - - return pairs + return samples.valid_sample_instance_pairs(minimum_weight=minimum_weight) def randomly_drawn_via_pdf_gen_from(self, total_samples: int): """ diff --git a/autofit/non_linear/samples/samples.py b/autofit/non_linear/samples/samples.py index f09be3800..acacd449b 100644 --- a/autofit/non_linear/samples/samples.py +++ b/autofit/non_linear/samples/samples.py @@ -80,6 +80,52 @@ def instances(self): for sample in self.sample_list ] + def valid_sample_instance_pairs( + self, + minimum_weight: float = float("-inf"), + ignore_assertions: bool = False, + ) -> List[Tuple[Sample, ModelInstance]]: + """Return stored samples paired with instances they can still build. + + Constructor validation can become stricter after a result was written. + Historical points rejected through the narrow :class:`FitException` + contract are therefore skipped, while programming errors continue to + propagate. + + Parameters + ---------- + minimum_weight + Only samples with a weight strictly greater than this value are + considered. + ignore_assertions + If ``True``, model assertions are not checked while constructing an + instance. Constructor validation still applies. + """ + pairs = [] + rejected = 0 + + for sample in self.sample_list: + if sample.weight <= minimum_weight: + continue + try: + instance = sample.instance_for_model( + model=self.model, + ignore_assertions=ignore_assertions, + ) + except exc.FitException: + rejected += 1 + continue + pairs.append((sample, instance)) + + if rejected: + logger.warning( + "Skipped %d stored sample(s) rejected by current model " + "validation while reconstructing instances.", + rejected, + ) + + return pairs + @property def log_evidence(self): return None diff --git a/test_autofit/non_linear/samples/test_samples.py b/test_autofit/non_linear/samples/test_samples.py index cf85d69c9..45033dc43 100644 --- a/test_autofit/non_linear/samples/test_samples.py +++ b/test_autofit/non_linear/samples/test_samples.py @@ -160,6 +160,51 @@ def test__max_log_likelihood__historical_invalid_best_uses_next_valid_instance() assert samples.max_log_likelihood().value == 0.9 +def test__valid_sample_instance_pairs__skips_historical_invalid_points(): + samples = _guarded_samples( + parameter_lists=[[0.1], [0.9]], + log_likelihood_list=[2.0, 1.0], + weight_list=[0.6, 0.4], + ) + + pairs = samples.valid_sample_instance_pairs() + + assert len(pairs) == 1 + assert pairs[0][0].weight == 0.4 + assert pairs[0][1].value == 0.9 + + +def test__valid_sample_instance_pairs__all_invalid_returns_empty(): + samples = _guarded_samples( + parameter_lists=[[0.1]], + log_likelihood_list=[1.0], + weight_list=[1.0], + ) + + assert samples.valid_sample_instance_pairs() == [] + + +def test__valid_sample_instance_pairs__does_not_hide_programming_errors(): + class _RaisesUnexpectedly: + def __init__(self, value): + raise ValueError("real bug") + + model = af.Model(_RaisesUnexpectedly) + samples = af.SamplesPDF( + model=model, + sample_list=af.Sample.from_lists( + model=model, + parameter_lists=[[1.0]], + log_likelihood_list=[1.0], + log_prior_list=[0.0], + weight_list=[1.0], + ), + ) + + with pytest.raises(ValueError, match="real bug"): + samples.valid_sample_instance_pairs() + + def test__draw_randomly_via_pdf__historical_invalid_draw_is_retried(monkeypatch): from autofit.non_linear.samples import pdf