From 5a452902db4ea24b6a643ab2ce2d5fc473ebdd3d Mon Sep 17 00:00:00 2001 From: Jammy2211 Date: Sun, 12 Apr 2026 20:33:32 +0100 Subject: [PATCH] refactor: replace YAML config with explicit Python defaults in search classes Remove per-search YAML config files (nest.yaml, mcmc.yaml, mle.yaml) and the config machinery that loaded them (config_dict_search, config_dict_run, config_type, _class_config, _config, setattr loop). All search parameters are now explicit typed __init__ arguments with Python defaults on each concrete search class (Nautilus, DynestyStatic, DynestyDynamic, Emcee, Zeus, BFGS/LBFGS, Drawer). Test mode uses apply_test_mode() for direct attribute mutation instead of dict manipulation. Co-Authored-By: Claude Opus 4.6 --- autofit/config/non_linear/mcmc.yaml | 60 -- autofit/config/non_linear/mle.yaml | 69 -- autofit/config/non_linear/nest.yaml | 88 --- autofit/non_linear/mock/mock_search.py | 9 - autofit/non_linear/search/abstract_search.py | 93 +-- .../non_linear/search/mcmc/abstract_mcmc.py | 126 ++-- .../search/mcmc/auto_correlations.py | 18 +- .../non_linear/search/mcmc/emcee/search.py | 70 +- autofit/non_linear/search/mcmc/zeus/search.py | 93 ++- autofit/non_linear/search/mle/abstract_mle.py | 12 +- autofit/non_linear/search/mle/bfgs/search.py | 658 +++++++++--------- .../non_linear/search/mle/drawer/search.py | 13 +- .../non_linear/search/nest/abstract_nest.py | 6 +- .../search/nest/dynesty/search/abstract.py | 135 ++-- .../search/nest/dynesty/search/dynamic.py | 46 +- .../search/nest/dynesty/search/static.py | 44 +- .../non_linear/search/nest/nautilus/search.py | 174 +++-- test_autofit/aggregator/test_reference.py | 6 +- test_autofit/config/non_linear/mcmc.yaml | 47 -- test_autofit/config/non_linear/mle.yaml | 28 - test_autofit/config/non_linear/mock.yaml | 11 - test_autofit/config/non_linear/nest.yaml | 79 --- .../database/identifier/test_identifiers.py | 4 +- .../graphical/regression/test_static.py | 4 - test_autofit/interpolator/test_covariance.py | 33 +- .../non_linear/search/mcmc/test_emcee.py | 89 ++- .../non_linear/search/mcmc/test_zeus.py | 91 ++- .../non_linear/search/nest/test_dynesty.py | 125 ++-- .../non_linear/search/nest/test_nautilus.py | 42 +- .../non_linear/search/optimize/test_drawer.py | 46 +- .../non_linear/search/optimize/test_lbfgs.py | 107 ++- .../non_linear/search/test_abstract_search.py | 8 +- test_autofit/non_linear/test_dict.py | 12 +- 33 files changed, 1037 insertions(+), 1409 deletions(-) delete mode 100644 autofit/config/non_linear/mcmc.yaml delete mode 100644 autofit/config/non_linear/mle.yaml delete mode 100644 autofit/config/non_linear/nest.yaml delete mode 100644 test_autofit/config/non_linear/mcmc.yaml delete mode 100644 test_autofit/config/non_linear/mle.yaml delete mode 100644 test_autofit/config/non_linear/mock.yaml delete mode 100644 test_autofit/config/non_linear/nest.yaml diff --git a/autofit/config/non_linear/mcmc.yaml b/autofit/config/non_linear/mcmc.yaml deleted file mode 100644 index 27d0c5e86..000000000 --- a/autofit/config/non_linear/mcmc.yaml +++ /dev/null @@ -1,60 +0,0 @@ -# Configuration files that customize the default behaviour of non-linear searches. - -# **PyAutoFit** supports the following MCMC algorithms: - -# - Emcee: https://github.com/dfm/emcee / https://emcee.readthedocs.io/en/stable/ -# - Zeus: https://github.com/minaskar/zeus / https://zeus-mcmc.readthedocs.io/en/latest/ - -# Settings in the [search] and [run] entries are specific to each nested algorithm and should be determined by -# consulting that MCMC method's own readthedocs. - -Emcee: - run: - nsteps: 2000 - search: - nwalkers: 50 - auto_correlations: - change_threshold: 0.01 # The threshold value by which if the change in auto_correlations is below sampling will be terminated early. - check_for_convergence: true # Whether the auto-correlation lengths of the Emcee samples are checked to determine the stopping criteria. If `True`, Emcee may stop before nsteps are performed. - check_size: 100 # The length of the samples used to check the auto-correlation lengths (from the latest sample backwards). - required_length: 50 # The length an auto_correlation chain must be for it to be used to evaluate whether its change threshold is sufficiently small to terminate sampling early. - initialize: # The method used to generate where walkers are initialized in parameter space {prior | ball}. - method: ball # priors: samples are initialized by randomly drawing from each parameter's prior. ball: samples are initialized by randomly drawing unit values from a narrow uniform distribution. - ball_lower_limit: 0.49 # The lower limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - ball_upper_limit: 0.51 # The upper limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - printing: - silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter. -Zeus: - run: - check_walkers: true - light_mode: false - maxiter: 10000 - maxsteps: 10000 - mu: 1.0 - nsteps: 2000 - patience: 5 - shuffle_ensemble: true - tolerance: 0.05 - tune: true - vectorize: false - search: - nwalkers: 50 - auto_correlations: - change_threshold: 0.01 # The threshold value by which if the change in auto_correlations is below sampling will be terminated early. - check_for_convergence: true # Whether the auto-correlation lengths of the Emcee samples are checked to determine the stopping criteria. If `True`, Emcee may stop before nsteps are performed. - check_size: 100 # The length of the samples used to check the auto-correlation lengths (from the latest sample backwards). - required_length: 50 # The length an auto_correlation chain must be for it to be used to evaluate whether its change threshold is sufficiently small to terminate sampling early. - initialize: # The method used to generate where walkers are initialized in parameter space {prior | ball}. - method: ball # priors: samples are initialized by randomly drawing from each parameter's prior. ball: samples are initialized by randomly drawing unit values from a narrow uniform distribution. - ball_lower_limit: 0.49 # The lower limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - ball_upper_limit: 0.51 # The upper limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - printing: - silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter. - - iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow. - iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit. - remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable). \ No newline at end of file diff --git a/autofit/config/non_linear/mle.yaml b/autofit/config/non_linear/mle.yaml deleted file mode 100644 index 0d1c4c08b..000000000 --- a/autofit/config/non_linear/mle.yaml +++ /dev/null @@ -1,69 +0,0 @@ -# Configuration files that customize the default behaviour of non-linear searches. - -# **PyAutoFit** supports the following maximum likelihood estimator (MLE) algorithms: - -# Settings in the [search], [run] and [options] entries are specific to each algorithm and should be -# determined by consulting that method's own readthedocs. - -BFGS: - search: - tol: null - options: - disp: false - eps: 1.0e-08 - ftol: 2.220446049250313e-09 - gtol: 1.0e-05 - iprint: -1.0 - maxcor: 10 - maxfun: 15000 - maxiter: 15000 - maxls: 20 - initialize: # The method used to generate where walkers are initialized in parameter space {prior | ball}. - method: ball # priors: samples are initialized by randomly drawing from each parameter's prior. ball: samples are initialized by randomly drawing unit values from a narrow uniform distribution. - ball_lower_limit: 0.49 # The lower limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - ball_upper_limit: 0.51 # The upper limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - printing: - silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter. - iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow. - iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit. - remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable). -LBFGS: - search: - tol: null - options: - disp: false - eps: 1.0e-08 - ftol: 2.220446049250313e-09 - gtol: 1.0e-05 - iprint: -1.0 - maxcor: 10 - maxfun: 15000 - maxiter: 15000 - maxls: 20 - initialize: # The method used to generate where walkers are initialized in parameter space {prior | ball}. - method: ball # priors: samples are initialized by randomly drawing from each parameter's prior. ball: samples are initialized by randomly drawing unit values from a narrow uniform distribution. - ball_lower_limit: 0.49 # The lower limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - ball_upper_limit: 0.51 # The upper limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - printing: - silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter. - iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow. - iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit. - remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable). -Drawer: - search: - total_draws: 50 - initialize: # The method used to generate where walkers are initialized in parameter space {prior | ball}. - method: ball # priors: samples are initialized by randomly drawing from each parameter's prior. ball: samples are initialized by randomly drawing unit values from a narrow uniform distribution. - ball_lower_limit: 0.49 # The lower limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - ball_upper_limit: 0.51 # The upper limit of the uniform distribution unit values are drawn from when initializing walkers using the ball method. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - printing: - silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter. - iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow. - iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit. - remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable). \ No newline at end of file diff --git a/autofit/config/non_linear/nest.yaml b/autofit/config/non_linear/nest.yaml deleted file mode 100644 index b59cbf486..000000000 --- a/autofit/config/non_linear/nest.yaml +++ /dev/null @@ -1,88 +0,0 @@ -# Configuration files that customize the default behaviour of non-linear searches. - -# - Dynesty: https://github.com/joshspeagle/dynesty / https://dynesty.readthedocs.io/en/latest/index.html -# - Nautilus https://https://github.com/johannesulf/nautilus / https://nautilus-sampler.readthedocs.io/en/stable/index.html - -# Settings in the [search] and [run] entries are specific to each nested algorithm and should be determined by -# consulting that MCMC method's own readthedocs. - -DynestyStatic: - search: - bootstrap: null - bound: multi - enlarge: null - facc: 0.2 - first_update: null - fmove: 0.9 - max_move: 100 - nlive: 50 - sample: auto - slices: 5 - update_interval: null - walks: 5 - run: - dlogz: null - logl_max: .inf - maxcall: null - maxiter: null - initialize: # The method used to generate where walkers are initialized in parameter space {prior}. - method: prior # priors: samples are initialized by randomly drawing from each parameter's prior. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems. - printing: - silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter. -DynestyDynamic: - search: - bootstrap: null - bound: multi - enlarge: null - facc: 0.2 - first_update: null - fmove: 0.9 - max_move: 100 - sample: auto - slices: 5 - update_interval: null - walks: 5 - run: - dlogz_init: 0.01 - logl_max_init: .inf - maxcall: null - maxcall_init: null - maxiter: null - maxiter_init: null - nlive_init: 500 - initialize: # The method used to generate where walkers are initialized in parameter space {prior}. - method: prior # priors: samples are initialized by randomly drawing from each parameter's prior. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems. - printing: - silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter. -Nautilus: - search: - n_live: 3000 # Number of so-called live points. New bounds are constructed so that they encompass the live points. - n_update: # The maximum number of additions to the live set before a new bound is created - enlarge_per_dim: 1.1 # Along each dimension, outer ellipsoidal bounds are enlarged by this factor. - n_points_min: # The minimum number of points each ellipsoid should have. Effectively, ellipsoids with less than twice that number will not be split further. - split_threshold: 100 # Threshold used for splitting the multi-ellipsoidal bound used for sampling. - n_networks: 4 # Number of networks used in the estimator. - n_batch: 100 # Number of likelihood evaluations that are performed at each step. If likelihood evaluations are parallelized, should be multiple of the number of parallel processes. - n_like_new_bound: # The maximum number of likelihood calls before a new bounds is created. If None, use 10 times n_live. - vectorized: false # If True, the likelihood function can receive multiple input sets at once. - seed: # Seed for random number generation used for reproducible results accross different runs. - run: - f_live: 0.01 # Maximum fraction of the evidence contained in the live set before building the initial shells terminates. - n_shell: 1 # Minimum number of points in each shell. The algorithm will sample from the shells until this is reached. Default is 1. - n_eff: 500 # Minimum effective sample size. The algorithm will sample from the shells until this is reached. Default is 10000. - n_like_max: .inf # Maximum number of likelihood evaluations. Regardless of progress, the sampler will stop if this value is reached. Default is infinity. - discard_exploration: false # Whether to discard points drawn in the exploration phase. This is required for a fully unbiased posterior and evidence estimate. - verbose: true # Whether to print information about the run. - initialize: # The method used to generate where walkers are initialized in parameter space {prior}. - method: prior # priors: samples are initialized by randomly drawing from each parameter's prior. - parallel: - number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing. - force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems. - printing: - silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter. diff --git a/autofit/non_linear/mock/mock_search.py b/autofit/non_linear/mock/mock_search.py index ab26c4a4b..0ae1c1018 100644 --- a/autofit/non_linear/mock/mock_search.py +++ b/autofit/non_linear/mock/mock_search.py @@ -1,6 +1,5 @@ from typing import Optional, Tuple -from autoconf import conf from autofit import exc from autofit.graphical import FactorApproximation from autofit.graphical.utils import Status @@ -69,14 +68,6 @@ def __init__( def check_model(self, model): pass - @property - def config_type(self): - return conf.instance["non_linear"]["mock"] - - @property - def config_dict_search(self): - return {} - def _fit_fast(self, model, analysis): class Fitness: def __init__(self, instance_from_vector, result): diff --git a/autofit/non_linear/search/abstract_search.py b/autofit/non_linear/search/abstract_search.py index d7c0f91d0..b418cdb55 100644 --- a/autofit/non_linear/search/abstract_search.py +++ b/autofit/non_linear/search/abstract_search.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from autofit.non_linear.result import Result -from autoconf import conf, cached_property +from autoconf import conf from autoconf.output import should_output @@ -49,7 +49,7 @@ from autofit.graphical.expectation_propagation import AbstractFactorOptimiser from autofit.non_linear.fitness import get_timeout_seconds -from autofit.non_linear.test_mode import is_test_mode, test_mode_level, skip_fit_output +from autofit.non_linear.test_mode import test_mode_level, skip_fit_output logger = logging.getLogger(__name__) @@ -131,12 +131,13 @@ def __init__( iterations_per_quick_update: Optional[int] = None, iterations_per_full_update: int = None, number_of_cores: int = 1, + silence: bool = False, session: Optional[sa.orm.Session] = None, paths: Optional[AbstractPaths] = None, **kwargs, ): """ - Abstract base class for non-linear searches.L + Abstract base class for non-linear searches. This class sets up the file structure for the non-linear search, which are standardized across all non-linear searches. @@ -152,6 +153,8 @@ def __init__( and also acts as the folder after the path prefix and before the search name. initializer Generates the initialize samples of non-linear parameter space (see autofit.non_linear.initializer). + silence + If True, the default print output of the non-linear search is silenced. session An SQLAlchemy session instance so the results of the model-fit are written to an SQLite database. """ @@ -202,15 +205,11 @@ def __init__( "force_visualize_overwrite" ] - if initializer is None: - self.logger.debug("Creating initializer ") - self.initializer = Initializer.from_config(config=self._config) - else: + if initializer is not None: self.initializer = initializer self.iterations_per_quick_update = float((iterations_per_quick_update or conf.instance["general"]["updates"]["iterations_per_quick_update"])) - self.iterations_per_full_update = float((iterations_per_full_update or conf.instance["general"]["updates"]["iterations_per_full_update"])) @@ -227,22 +226,13 @@ def __init__( self.should_profile = conf.instance["general"]["profiling"]["should_profile"] - self.silence = self._config("printing", "silence") + self.silence = silence if conf.instance["general"]["hpc"]["hpc_mode"]: self.silence = True self.kwargs = kwargs - for key, value in self.config_dict_search.items(): - setattr(self, key, value) - - try: - for key, value in self.config_dict_run.items(): - setattr(self, key, value) - except KeyError: - pass - self.number_of_cores = number_of_cores if number_of_cores > 1 and any( @@ -511,7 +501,6 @@ class represented by model M and gives a score for their fitness. # `visualize_before_fit` calls that `pre_fit_output` would add. if hasattr(self.paths, "save_all"): self.paths.save_all( - search_config_dict=self.config_dict_search, info=info, ) @@ -609,7 +598,6 @@ def pre_fit_output( ) self.paths.save_all( - search_config_dict=self.config_dict_search, info=info, ) analysis.save_attributes(paths=self.paths) @@ -924,68 +912,15 @@ def check_model(self, model: AbstractPriorModel): if model is not None and model.prior_count == 0: raise AssertionError("Model has no priors! Cannot fit a 0 dimension model.") - def config_dict_test_mode_from(self, config_dict: Dict) -> Dict: - raise NotImplementedError - - @property - def _class_config(self) -> Dict: - return self.config_type[self.__class__.__name__] - - @cached_property - def config_dict_search(self) -> Dict: - config_dict = copy.deepcopy(self._class_config["search"]) - - for key, value in config_dict.items(): - try: - config_dict[key] = self.kwargs[key] - except KeyError: - pass - - return config_dict - - @cached_property - def config_dict_run(self) -> Dict: - config_dict = copy.deepcopy(self._class_config["run"]) - - for key, value in config_dict.items(): - try: - config_dict[key] = self.kwargs[key] - except KeyError: - pass - - if is_test_mode(): - logger.warning( - "TEST MODE 1 (reduced iterations): Sampler will run with " - "minimal iterations for faster completion." - ) - - config_dict = self.config_dict_test_mode_from(config_dict=config_dict) - - return config_dict - - @property - def config_dict_settings(self) -> Dict: - return self._class_config["settings"] - - @property - def config_type(self): - raise NotImplementedError() - - def _config(self, section, attribute_name): + def apply_test_mode(self): """ - Get a config field from this search's section in non_linear.ini by a key and value type. - - Parameters - ---------- - attribute_name: str - The analysis_path of the field + Override in subclasses to reduce sampler iterations for test mode. - Returns - ------- - attribute - An attribute for the key with the specified type. + Called during __init__ when test mode is active (level 1). + Subclasses should directly mutate instance attributes to minimize + the number of iterations the sampler performs. """ - return self._class_config[section][attribute_name] + pass def output_search_internal(self, search_internal): self.paths.save_search_internal( diff --git a/autofit/non_linear/search/mcmc/abstract_mcmc.py b/autofit/non_linear/search/mcmc/abstract_mcmc.py index 6dcfffd7a..e305effb4 100644 --- a/autofit/non_linear/search/mcmc/abstract_mcmc.py +++ b/autofit/non_linear/search/mcmc/abstract_mcmc.py @@ -1,65 +1,61 @@ -from typing import Optional - -from autoconf import conf -from autofit.database.sqlalchemy_ import sa -from autofit.non_linear.search.abstract_search import NonLinearSearch -from autofit.non_linear.initializer import Initializer -from autofit.non_linear.samples import SamplesMCMC -from autofit.non_linear.search.mcmc.auto_correlations import AutoCorrelationsSettings -from autofit.non_linear.plot import corner_cornerpy - -class AbstractMCMC(NonLinearSearch): - - def __init__( - self, - name: Optional[str] = None, - path_prefix: Optional[str] = None, - unique_tag: Optional[str] = None, - initializer: Optional[Initializer] = None, - auto_correlation_settings=AutoCorrelationsSettings(), - iterations_per_full_update: Optional[int] = None, - iterations_per_quick_update: int = None, - number_of_cores: Optional[int] = None, - session: Optional[sa.orm.Session] = None, - **kwargs - ): - - self.auto_correlation_settings = auto_correlation_settings - self.auto_correlation_settings.update_via_config( - config=self.config_type[self.__class__.__name__]["auto_correlations"] - ) - - super().__init__( - name=name, - path_prefix=path_prefix, - unique_tag=unique_tag, - initializer=initializer, - iterations_per_quick_update=iterations_per_quick_update, - iterations_per_full_update=iterations_per_full_update, - number_of_cores=number_of_cores, - session=session, - **kwargs - ) - - @property - def config_type(self): - return conf.instance["non_linear"]["mcmc"] - - @property - def samples_cls(self): - return SamplesMCMC - - def plot_results(self, samples): - - if not samples.pdf_converged: - return - - def should_plot(name): - return conf.instance["visualize"]["plots_search"]["mcmc"][name] - - if should_plot("corner_cornerpy"): - corner_cornerpy( - samples=samples, - path=self.paths.image_path / "search", - format="png", - ) +from typing import Optional + +from autoconf import conf +from autofit.database.sqlalchemy_ import sa +from autofit.non_linear.search.abstract_search import NonLinearSearch +from autofit.non_linear.initializer import Initializer, InitializerBall +from autofit.non_linear.samples import SamplesMCMC +from autofit.non_linear.search.mcmc.auto_correlations import AutoCorrelationsSettings +from autofit.non_linear.plot import corner_cornerpy + +class AbstractMCMC(NonLinearSearch): + + def __init__( + self, + name: Optional[str] = None, + path_prefix: Optional[str] = None, + unique_tag: Optional[str] = None, + initializer: Optional[Initializer] = None, + auto_correlation_settings=AutoCorrelationsSettings(), + iterations_per_full_update: Optional[int] = None, + iterations_per_quick_update: int = None, + number_of_cores: int = 1, + silence: bool = False, + session: Optional[sa.orm.Session] = None, + **kwargs + ): + self.auto_correlation_settings = auto_correlation_settings + + super().__init__( + name=name, + path_prefix=path_prefix, + unique_tag=unique_tag, + initializer=initializer or InitializerBall( + lower_limit=0.49, upper_limit=0.51 + ), + iterations_per_quick_update=iterations_per_quick_update, + iterations_per_full_update=iterations_per_full_update, + number_of_cores=number_of_cores, + silence=silence, + session=session, + **kwargs + ) + + @property + def samples_cls(self): + return SamplesMCMC + + def plot_results(self, samples): + + if not samples.pdf_converged: + return + + def should_plot(name): + return conf.instance["visualize"]["plots_search"]["mcmc"][name] + + if should_plot("corner_cornerpy"): + corner_cornerpy( + samples=samples, + path=self.paths.image_path / "search", + format="png", + ) diff --git a/autofit/non_linear/search/mcmc/auto_correlations.py b/autofit/non_linear/search/mcmc/auto_correlations.py index 3b17a8705..7c17fdde2 100644 --- a/autofit/non_linear/search/mcmc/auto_correlations.py +++ b/autofit/non_linear/search/mcmc/auto_correlations.py @@ -9,10 +9,10 @@ class AutoCorrelationsSettings: def __init__( self, - check_for_convergence: Optional[bool] = None, - check_size: Optional[int] = None, - required_length: Optional[int] = None, - change_threshold: Optional[float] = None, + check_for_convergence: bool = True, + check_size: int = 100, + required_length: int = 50, + change_threshold: float = 0.01, ): """ Class for performing and customizing AutoCorrelation calculations, which are used: @@ -42,19 +42,9 @@ def __init__( self.required_length = required_length self.change_threshold = change_threshold - def update_via_config(self, config): - - config_dict = config - - self.check_for_convergence = self.check_for_convergence if self.check_for_convergence is not None else config_dict["check_for_convergence"] - self.check_size = self.check_size or config_dict["check_size"] - if is_test_mode(): self.check_size = 1 - self.required_length = self.required_length or config_dict["required_length"] - self.change_threshold = self.change_threshold or config_dict["change_threshold"] - class AutoCorrelations(AutoCorrelationsSettings): diff --git a/autofit/non_linear/search/mcmc/emcee/search.py b/autofit/non_linear/search/mcmc/emcee/search.py index 34ebf932d..29140bf5b 100644 --- a/autofit/non_linear/search/mcmc/emcee/search.py +++ b/autofit/non_linear/search/mcmc/emcee/search.py @@ -1,3 +1,4 @@ +import logging import os from typing import Dict, Optional @@ -17,6 +18,8 @@ from autofit.non_linear.samples.sample import Sample from autofit.non_linear.samples.mcmc import SamplesMCMC +logger = logging.getLogger(__name__) + class Emcee(AbstractMCMC): __identifier_fields__ = ("nwalkers",) @@ -26,11 +29,14 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + nwalkers: int = 50, + nsteps: int = 2000, initializer: Optional[Initializer] = None, auto_correlation_settings=AutoCorrelationsSettings(), iterations_per_quick_update: int = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, session: Optional[sa.orm.Session] = None, **kwargs, ): @@ -55,22 +61,22 @@ def __init__( unique_tag The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database and also acts as the folder after the path prefix and before the search name. + nwalkers + The number of walkers in the ensemble used to sample parameter space. + nsteps + The number of steps that must be taken by every walker. initializer Generates the initialize samples of non-linear parameter space (see autofit.non_linear.initializer). auto_correlation_settings Customizes and performs auto correlation calculations performed during and after the search. number_of_cores The number of cores sampling is performed using a Python multiprocessing Pool instance. + silence + If True, the default print output of the non-linear search is silenced. session An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ - number_of_cores = ( - self._config("parallel", "number_of_cores") - if number_of_cores is None - else number_of_cores - ) - super().__init__( name=name, path_prefix=path_prefix, @@ -80,17 +86,29 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs, ) - self.logger.debug("Creating Emcee Search") + self.nwalkers = nwalkers + self.nsteps = nsteps - # TODO : Emcee visualization tools rely on the .hdf file and thus require that the search internal is - # TODO : On hard-disk, which this forces to occur. + if is_test_mode(): + self.apply_test_mode() + + self.logger.debug("Creating Emcee Search") conf.instance["output"]["search_internal"] = True + def apply_test_mode(self): + logger.warning( + "TEST MODE 1 (reduced iterations): Sampler will run with " + "minimal iterations for faster completion." + ) + self.nwalkers = 20 + self.nsteps = 10 + def _fit(self, model: AbstractPriorModel, analysis): """ Fit a model using Emcee and the Analysis class which contains the data and returns the log likelihood from @@ -127,7 +145,7 @@ def _fit(self, model: AbstractPriorModel, analysis): backend = None search_internal = emcee.EnsembleSampler( - nwalkers=self.config_dict_search["nwalkers"], + nwalkers=self.nwalkers, ndim=model.prior_count, log_prob_fn=fitness.call_wrap, backend=backend, @@ -143,7 +161,7 @@ def _fit(self, model: AbstractPriorModel, analysis): if samples.converged: iterations_remaining = 0 else: - iterations_remaining = self.config_dict_run["nsteps"] - total_iterations + iterations_remaining = self.nsteps - total_iterations self.logger.info( "Resuming Emcee non-linear search (previous samples found)." @@ -178,7 +196,7 @@ def _fit(self, model: AbstractPriorModel, analysis): state[index, :] = np.asarray(parameters) total_iterations = 0 - iterations_remaining = self.config_dict_run["nsteps"] + iterations_remaining = self.nsteps while iterations_remaining > 0: if self.iterations_per_full_update > iterations_remaining: @@ -198,7 +216,7 @@ def _fit(self, model: AbstractPriorModel, analysis): state = search_internal.get_last_sample() total_iterations += iterations - iterations_remaining = self.config_dict_run["nsteps"] - total_iterations + iterations_remaining = self.nsteps - total_iterations samples = self.samples_from(model=model, search_internal=search_internal) @@ -340,30 +358,6 @@ def auto_correlations_from(self, search_internal=None): previous_times=previous_auto_correlation_times, ) - def config_dict_test_mode_from(self, config_dict: Dict) -> Dict: - """ - Returns a configuration dictionary for test mode meaning that the sampler terminates as quickly as possible. - - Entries which set the total number of samples of the sampler (e.g. maximum calls, maximum likelihood - evaluations) are reduced to low values meaning it terminates nearly immediately. - - Parameters - ---------- - config_dict - The original configuration dictionary for this sampler which includes entries controlling how fast the - sampler terminates. - - Returns - ------- - A configuration dictionary where settings which control the sampler's number of samples are reduced so it - terminates as quickly as possible. - """ - return { - **config_dict, - "nwalkers": 20, - "nsteps": 10, - } - @property def backend_filename(self): return self.paths.search_internal_path / "search_internal.hdf" diff --git a/autofit/non_linear/search/mcmc/zeus/search.py b/autofit/non_linear/search/mcmc/zeus/search.py index 5c37420ea..27667b889 100644 --- a/autofit/non_linear/search/mcmc/zeus/search.py +++ b/autofit/non_linear/search/mcmc/zeus/search.py @@ -16,6 +16,8 @@ from autofit.non_linear.test_mode import is_test_mode from autofit.non_linear.samples.mcmc import SamplesMCMC +logger = logging.getLogger(__name__) + class Zeus(AbstractMCMC): __identifier_fields__ = ( @@ -32,16 +34,30 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + nwalkers: int = 50, + nsteps: int = 2000, + tune: bool = True, + tolerance: float = 0.05, + patience: int = 5, + mu: float = 1.0, + light_mode: bool = False, + maxsteps: int = 10000, + maxiter: int = 10000, + vectorize: bool = False, + shuffle_ensemble: bool = True, + check_walkers: bool = True, + maxcall: Optional[int] = None, initializer: Optional[Initializer] = None, auto_correlation_settings=AutoCorrelationsSettings(), iterations_per_quick_update: int = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, session: Optional[sa.orm.Session] = None, **kwargs ): """ - An Zeus non-linear search. + A Zeus non-linear search. For a full description of Zeus, checkout its Github and readthedocs webpages: @@ -64,21 +80,19 @@ def __init__( nwalkers The number of walkers in the ensemble used to sample parameter space. nsteps - The number of steps that must be taken by every walker. The `NonLinearSearch` will thus run for nwalkers * - nsteps iterations. + The number of steps that must be taken by every walker. initializer Generates the initialize samples of non-linear parameter space (see autofit.non_linear.initializer). - auto_correlation_settings : AutoCorrelationsSettings + auto_correlation_settings Customizes and performs auto correlation calculations performed during and after the search. number_of_cores - The number of cores Zeus sampling is performed using a Python multiprocessing Pool instance. If 1, a - pool instance is not created and the job runs in serial. + The number of cores Zeus sampling is performed using a Python multiprocessing Pool instance. + silence + If True, the default print output of the non-linear search is silenced. session An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ - number_of_cores = number_of_cores or self._config("parallel", "number_of_cores") - super().__init__( name=name, path_prefix=path_prefix, @@ -88,12 +102,38 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs ) + self.nwalkers = nwalkers + self.nsteps = nsteps + self.tune = tune + self.tolerance = tolerance + self.patience = patience + self.mu = mu + self.light_mode = light_mode + self.maxsteps = maxsteps + self.maxiter = maxiter + self.vectorize = vectorize + self.shuffle_ensemble = shuffle_ensemble + self.check_walkers = check_walkers + self.maxcall = maxcall + + if is_test_mode(): + self.apply_test_mode() + self.logger.debug("Creating Zeus Search") + def apply_test_mode(self): + logger.warning( + "TEST MODE 1 (reduced iterations): Sampler will run with " + "minimal iterations for faster completion." + ) + self.nwalkers = 20 + self.nsteps = 10 + def _fit(self, model: AbstractPriorModel, analysis): """ Fit a model using Zeus and the Analysis class which contains the data and returns the log likelihood from @@ -148,7 +188,7 @@ def _fit(self, model: AbstractPriorModel, analysis): if samples.converged: iterations_remaining = 0 else: - iterations_remaining = self.config_dict_run["nsteps"] - total_iterations + iterations_remaining = self.nsteps - total_iterations self.logger.info( "Resuming Zeus non-linear search (previous samples found)." @@ -156,7 +196,7 @@ def _fit(self, model: AbstractPriorModel, analysis): except (FileNotFoundError, AttributeError): search_internal = zeus.EnsembleSampler( - nwalkers=self.config_dict_search["nwalkers"], + nwalkers=self.nwalkers, ndim=model.prior_count, logprob_fn=fitness.call_wrap, pool=pool, @@ -193,7 +233,7 @@ def _fit(self, model: AbstractPriorModel, analysis): state[index, :] = np.asarray(parameters) total_iterations = 0 - iterations_remaining = self.config_dict_run["nsteps"] + iterations_remaining = self.nsteps while iterations_remaining > 0: if self.iterations_per_full_update > iterations_remaining: @@ -219,7 +259,7 @@ def _fit(self, model: AbstractPriorModel, analysis): log_posterior_list = search_internal.get_last_log_prob() total_iterations += iterations - iterations_remaining = self.config_dict_run["nsteps"] - total_iterations + iterations_remaining = self.nsteps - total_iterations samples = self.samples_from(model=model, search_internal=search_internal) @@ -239,8 +279,8 @@ def _fit(self, model: AbstractPriorModel, analysis): thin = int(np.max(auto_correlation_time) / 2.0) chain = search_internal.get_chain(discard=discard, thin=thin, flat=True) - if "maxcall" in self.kwargs: - if search_internal.ncall_total > self.kwargs["maxcall"]: + if self.maxcall is not None: + if search_internal.ncall_total > self.maxcall: iterations_remaining = 0 if iterations_remaining > 0: @@ -373,26 +413,3 @@ def auto_correlations_from(self, search_internal=None): previous_times=previous_auto_correlation_times, ) - def config_dict_test_mode_from(self, config_dict: Dict) -> Dict: - """ - Returns a configuration dictionary for test mode meaning that the sampler terminates as quickly as possible. - - Entries which set the total number of samples of the sampler (e.g. maximum calls, maximum likelihood - evaluations) are reduced to low values meaning it terminates nearly immediately. - - Parameters - ---------- - config_dict - The original configuration dictionary for this sampler which includes entries controlling how fast the - sampler terminates. - - Returns - ------- - A configuration dictionary where settings which control the sampler's number of samples are reduced so it - terminates as quickly as possible. - """ - return { - **config_dict, - "nwalkers": 20, - "nsteps": 10, - } diff --git a/autofit/non_linear/search/mle/abstract_mle.py b/autofit/non_linear/search/mle/abstract_mle.py index 36ac52828..c72351f37 100644 --- a/autofit/non_linear/search/mle/abstract_mle.py +++ b/autofit/non_linear/search/mle/abstract_mle.py @@ -2,14 +2,20 @@ from autoconf import conf from autofit.non_linear.search.abstract_search import NonLinearSearch +from autofit.non_linear.initializer import InitializerBall from autofit.non_linear.samples import Samples from autofit.non_linear.plot import subplot_parameters, log_likelihood_vs_iteration class AbstractMLE(NonLinearSearch, ABC): - @property - def config_type(self): - return conf.instance["non_linear"]["mle"] + + def __init__(self, initializer=None, **kwargs): + super().__init__( + initializer=initializer or InitializerBall( + lower_limit=0.49, upper_limit=0.51 + ), + **kwargs, + ) @property def samples_cls(self): diff --git a/autofit/non_linear/search/mle/bfgs/search.py b/autofit/non_linear/search/mle/bfgs/search.py index 2b105e786..74ab8aafc 100644 --- a/autofit/non_linear/search/mle/bfgs/search.py +++ b/autofit/non_linear/search/mle/bfgs/search.py @@ -1,347 +1,311 @@ -from typing import Optional - -from autoconf import cached_property -from autofit.database.sqlalchemy_ import sa - -from autofit.mapper.prior_model.abstract import AbstractPriorModel -from autofit.non_linear.search.mle.abstract_mle import AbstractMLE -from autofit.non_linear.analysis import Analysis -from autofit.non_linear.fitness import Fitness -from autofit.non_linear.initializer import AbstractInitializer -from autofit.non_linear.samples.sample import Sample -from autofit.non_linear.samples.samples import Samples - -import copy -import numpy as np - - -class AbstractBFGS(AbstractMLE): - - method = None - - def __init__( - self, - name: Optional[str] = None, - path_prefix: Optional[str] = None, - unique_tag: Optional[str] = None, - initializer: Optional[AbstractInitializer] = None, - iterations_per_full_update: int = None, - iterations_per_quick_update: int = None, - session: Optional[sa.orm.Session] = None, - **kwargs - ): - """ - Abstract wrapper for the BFGS and L-BFGS scipy non-linear searches. - - See the docstrings of the `BFGS` and `LBFGS` classes for a description of the arguments of this class. - """ - - super().__init__( - name=name, - path_prefix=path_prefix, - unique_tag=unique_tag, - initializer=initializer, - iterations_per_quick_update=iterations_per_quick_update, - iterations_per_full_update=iterations_per_full_update, - session=session, - **kwargs - ) - - self.logger.debug(f"Creating {self.method} Search") - - @cached_property - def config_dict_options(self): - config_dict = copy.deepcopy(self._class_config["options"]) - - for key, value in config_dict.items(): - try: - config_dict[key] = self.kwargs[key] - except KeyError: - pass - - return config_dict - - def _fit( - self, - model: AbstractPriorModel, - analysis: Analysis, - ): - """ - Fit a model using the scipy L-BFGS method and the Analysis class which contains the data and returns the log - likelihood from instances of the model, which the `NonLinearSearch` seeks to maximize. - - Parameters - ---------- - model - The model which generates instances for different points in parameter space. - analysis - Contains the data and the log likelihood function which fits an instance of the model to the data, - returning the log likelihood the `NonLinearSearch` maximizes. - - Returns - ------- - A result object comprising the Samples object that inclues the maximum log likelihood instance and full - chains used by the fit. - """ - from scipy import optimize - - fitness = Fitness( - model=model, - analysis=analysis, - paths=self.paths, - fom_is_log_likelihood=False, - resample_figure_of_merit=-np.inf, - convert_to_chi_squared=True, - store_history=self.should_plot_start_point - ) - - try: - search_internal_dict = self.paths.load_search_internal() - - x0 = search_internal_dict["x0"] - total_iterations = search_internal_dict["total_iterations"] - - self.logger.info( - "Resuming LBFGS non-linear search (previous samples found)." - ) - - except (FileNotFoundError, TypeError): - - ( - unit_parameter_lists, - parameter_lists, - log_posterior_list, - ) = self.initializer.samples_from_model( - total_points=1, - model=model, - fitness=fitness, - paths=self.paths, - n_cores=self.number_of_cores, - ) - - x0 = np.asarray(parameter_lists[0]) - - total_iterations = 0 - - self.logger.info( - f"Starting new {self.method} non-linear search (no previous samples found)." - ) - - self.plot_start_point( - parameter_vector=x0, - model=model, - analysis=analysis, - ) - - maxiter = self.config_dict_options.get("maxiter", 1e8) - - while total_iterations < maxiter: - - iterations_remaining = maxiter - total_iterations - iterations = min(self.iterations_per_full_update, iterations_remaining) - - if iterations > 0: - config_dict_options = self.config_dict_options - config_dict_options["maxiter"] = iterations - - if analysis._use_jax: - - search_internal = optimize.minimize( - fun=fitness._jit, - x0=x0, - method=self.method, - options=config_dict_options, - **self.config_dict_search - ) - else: - - search_internal = optimize.minimize( - fun=fitness.__call__, - x0=x0, - method=self.method, - options=config_dict_options, - **self.config_dict_search - ) - - total_iterations += search_internal.nit - - search_internal.log_posterior_list = -0.5 * fitness( - parameters=search_internal.x - ) - - if self.should_plot_start_point: - - search_internal.parameters_history_list = fitness.parameters_history_list - search_internal.log_likelihood_history_list = fitness.log_likelihood_history_list - - self.paths.save_search_internal( - obj=search_internal, - ) - - x0 = search_internal.x - - if search_internal.nit < iterations: - return search_internal, fitness - - self.perform_update( - model=model, - analysis=analysis, - during_analysis=True, - fitness=fitness, - search_internal=search_internal, - ) - - self.logger.info(f"{self.method} sampling complete.") - - return search_internal, fitness - - def samples_via_internal_from( - self, model: AbstractPriorModel, search_internal=None - ): - """ - Returns a `Samples` object from the LBFGS internal results. - - The samples contain all information on the parameter space sampling (e.g. the parameters, - log likelihoods, etc.). - - The internal search results are converted from the native format used by the search to lists of values - (e.g. `parameter_lists`, `log_likelihood_list`). - - Parameters - ---------- - model - Maps input vectors of unit parameter values to physical values and model instances via priors. - """ - - if search_internal is None: - search_internal = self.paths.load_search_internal() - - x0 = search_internal.x - total_iterations = search_internal.nit - - if self.should_plot_start_point: - - parameter_lists = search_internal.parameters_history_list - log_prior_list = model.log_prior_list_from(parameter_lists=parameter_lists) - log_likelihood_list = search_internal.log_likelihood_history_list - - else: - - parameter_lists = [list(x0)] - log_prior_list = model.log_prior_list_from(parameter_lists=parameter_lists) - log_posterior_list = np.array([search_internal.log_posterior_list]) - log_likelihood_list = [ - lp - prior for lp, prior in zip(log_posterior_list, log_prior_list) - ] - - weight_list = len(log_likelihood_list) * [1.0] - - sample_list = Sample.from_lists( - model=model, - parameter_lists=parameter_lists, - log_likelihood_list=log_likelihood_list, - log_prior_list=log_prior_list, - weight_list=weight_list, - ) - - samples_info = { - "total_iterations": total_iterations, - "time": self.timer.time if self.timer else None, - } - - return Samples( - model=model, - sample_list=sample_list, - samples_info=samples_info, - ) - - -class BFGS(AbstractBFGS): - """ - The BFGS non-linear search, which wraps the scipy Broyden-Fletcher-Goldfarb-Shanno (BFGS) algorithm. - - See the docstrings of the `BFGS` and `LBFGS` classes for a description of the arguments of this class. - - For a full description of the scipy BFGS method, checkout its documentation: - - https://docs.scipy.org/doc/scipy/reference/optimize.minimize-bfgs.html#optimize-minimize-bfgs - - If you use `BFGS` as part of a published work, please cite the package via scipy following the instructions - under the *Attribution* section of the GitHub page. - - By default, the BFGS method scipy implementation does not store the history of parameter values and - log likelihood values during the non-linear search. This is because storing these values can require a large - amount of memory, in contradiction to the BFGS method's primary advantage of being memory efficient. - This means that it is difficult to visualize the BFGS method results (e.g. log likelihood vs iteration). - - **PyAutoFit** extends the class with the option of using visualize mode, which stores the history of parameter - values and log likelihood values during the non-linear search. This allows the results of the BFGS method to be - visualized after the search has completed, and it is enabled by setting the `visualize` flag to `True`. - - Parameters - ---------- - name - The name of the search, controlling the last folder results are output. - path_prefix - The path of folders prefixing the name folder where results are output. - unique_tag - The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database - and also acts as the folder after the path prefix and before the search name. - initializer - Generates the initialize samples of non-linear parameter space (see autofit.non_linear.initializer). - number_of_cores: int - The number of cores sampling is performed using a Python multiprocessing Pool instance. - session - An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. - visualize - If True, visualization of the search is enabled, which requires storing the history of parameter values and - log likelihood values during the non-linear search. - """ - - method = "BFGS" - - -class LBFGS(AbstractBFGS): - """ - The L-BFGS non-linear search, which wraps the scipy Limited-memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) - algorithm. - - See the docstrings of the `BFGS` and `LBFGS` classes for a description of the arguments of this class. - - For a full description of the scipy L-BFGS method, checkout its documentation: - - https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html - - If you use `LBFGS` as part of a published work, please cite the package via scipy following the instructions - under the *Attribution* section of the GitHub page. - - By default, the L-BFGS method scipy implementation does not store the history of parameter values and - log likelihood values during the non-linear search. This is because storing these values can require a large - amount of memory, in contradiction to the L-BFGS method's primary advantage of being memory efficient. - This means that it is difficult to visualize the L-BFGS method results (e.g. log likelihood vs iteration). - - **PyAutoFit** extends the class with the option of using visualize mode, which stores the history of parameter - values and log likelihood values during the non-linear search. This allows the results of the L-BFGS method to be - visualized after the search has completed, and it is enabled by setting the `visualize` flag to `True`. - - Parameters - ---------- - name - The name of the search, controlling the last folder results are output. - path_prefix - The path of folders prefixing the name folder where results are output. - unique_tag - The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database - and also acts as the folder after the path prefix and before the search name. - initializer - Generates the initialize samples of non-linear parameter space (see autofit.non_linear.initializer). - number_of_cores: int - The number of cores sampling is performed using a Python multiprocessing Pool instance. - session - An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. - visualize - If True, visualization of the search is enabled, which requires storing the history of parameter values and - log likelihood values during the non-linear search. - """ - - method = "L-BFGS-B" \ No newline at end of file +from typing import Optional + +from autofit.database.sqlalchemy_ import sa + +from autofit.mapper.prior_model.abstract import AbstractPriorModel +from autofit.non_linear.search.mle.abstract_mle import AbstractMLE +from autofit.non_linear.analysis import Analysis +from autofit.non_linear.fitness import Fitness +from autofit.non_linear.initializer import AbstractInitializer +from autofit.non_linear.samples.sample import Sample +from autofit.non_linear.samples.samples import Samples + +import numpy as np + + +class AbstractBFGS(AbstractMLE): + + method = None + + def __init__( + self, + name: Optional[str] = None, + path_prefix: Optional[str] = None, + unique_tag: Optional[str] = None, + tol: Optional[float] = None, + disp: bool = False, + eps: float = 1.0e-08, + ftol: float = 2.220446049250313e-09, + gtol: float = 1.0e-05, + iprint: float = -1.0, + maxcor: int = 10, + maxfun: int = 15000, + maxiter: int = 15000, + maxls: int = 20, + initializer: Optional[AbstractInitializer] = None, + iterations_per_full_update: int = None, + iterations_per_quick_update: int = None, + silence: bool = False, + session: Optional[sa.orm.Session] = None, + **kwargs + ): + """ + Abstract wrapper for the BFGS and L-BFGS scipy non-linear searches. + + Parameters + ---------- + tol + Tolerance for termination. + disp + Set to True to print convergence messages. + maxiter + Maximum number of iterations. + maxfun + Maximum number of function evaluations. + """ + + super().__init__( + name=name, + path_prefix=path_prefix, + unique_tag=unique_tag, + initializer=initializer, + iterations_per_quick_update=iterations_per_quick_update, + iterations_per_full_update=iterations_per_full_update, + silence=silence, + session=session, + **kwargs + ) + + self.tol = tol + self.disp = disp + self.eps = eps + self.ftol = ftol + self.gtol = gtol + self.iprint = iprint + self.maxcor = maxcor + self.maxfun = maxfun + self.maxiter = maxiter + self.maxls = maxls + + self.logger.debug(f"Creating {self.method} Search") + + @property + def options(self): + return { + "disp": self.disp, + "eps": self.eps, + "ftol": self.ftol, + "gtol": self.gtol, + "iprint": self.iprint, + "maxcor": self.maxcor, + "maxfun": self.maxfun, + "maxiter": self.maxiter, + "maxls": self.maxls, + } + + def _fit( + self, + model: AbstractPriorModel, + analysis: Analysis, + ): + """ + Fit a model using the scipy L-BFGS method and the Analysis class which contains the data and returns the log + likelihood from instances of the model, which the `NonLinearSearch` seeks to maximize. + + Parameters + ---------- + model + The model which generates instances for different points in parameter space. + analysis + Contains the data and the log likelihood function which fits an instance of the model to the data, + returning the log likelihood the `NonLinearSearch` maximizes. + + Returns + ------- + A result object comprising the Samples object that inclues the maximum log likelihood instance and full + chains used by the fit. + """ + from scipy import optimize + + fitness = Fitness( + model=model, + analysis=analysis, + paths=self.paths, + fom_is_log_likelihood=False, + resample_figure_of_merit=-np.inf, + convert_to_chi_squared=True, + store_history=self.should_plot_start_point + ) + + try: + search_internal_dict = self.paths.load_search_internal() + + x0 = search_internal_dict["x0"] + total_iterations = search_internal_dict["total_iterations"] + + self.logger.info( + "Resuming LBFGS non-linear search (previous samples found)." + ) + + except (FileNotFoundError, TypeError): + + ( + unit_parameter_lists, + parameter_lists, + log_posterior_list, + ) = self.initializer.samples_from_model( + total_points=1, + model=model, + fitness=fitness, + paths=self.paths, + n_cores=self.number_of_cores, + ) + + x0 = np.asarray(parameter_lists[0]) + + total_iterations = 0 + + self.logger.info( + f"Starting new {self.method} non-linear search (no previous samples found)." + ) + + self.plot_start_point( + parameter_vector=x0, + model=model, + analysis=analysis, + ) + + while total_iterations < self.maxiter: + + iterations_remaining = self.maxiter - total_iterations + iterations = min(self.iterations_per_full_update, iterations_remaining) + + if iterations > 0: + options = dict(self.options) + options["maxiter"] = iterations + + if analysis._use_jax: + + search_internal = optimize.minimize( + fun=fitness._jit, + x0=x0, + method=self.method, + options=options, + tol=self.tol, + ) + else: + + search_internal = optimize.minimize( + fun=fitness.__call__, + x0=x0, + method=self.method, + options=options, + tol=self.tol, + ) + + total_iterations += search_internal.nit + + search_internal.log_posterior_list = -0.5 * fitness( + parameters=search_internal.x + ) + + if self.should_plot_start_point: + + search_internal.parameters_history_list = fitness.parameters_history_list + search_internal.log_likelihood_history_list = fitness.log_likelihood_history_list + + self.paths.save_search_internal( + obj=search_internal, + ) + + x0 = search_internal.x + + if search_internal.nit < iterations: + return search_internal, fitness + + self.perform_update( + model=model, + analysis=analysis, + during_analysis=True, + fitness=fitness, + search_internal=search_internal, + ) + + self.logger.info(f"{self.method} sampling complete.") + + return search_internal, fitness + + def samples_via_internal_from( + self, model: AbstractPriorModel, search_internal=None + ): + """ + Returns a `Samples` object from the LBFGS internal results. + + The samples contain all information on the parameter space sampling (e.g. the parameters, + log likelihoods, etc.). + + The internal search results are converted from the native format used by the search to lists of values + (e.g. `parameter_lists`, `log_likelihood_list`). + + Parameters + ---------- + model + Maps input vectors of unit parameter values to physical values and model instances via priors. + """ + + if search_internal is None: + search_internal = self.paths.load_search_internal() + + x0 = search_internal.x + total_iterations = search_internal.nit + + if self.should_plot_start_point: + + parameter_lists = search_internal.parameters_history_list + log_prior_list = model.log_prior_list_from(parameter_lists=parameter_lists) + log_likelihood_list = search_internal.log_likelihood_history_list + + else: + + parameter_lists = [list(x0)] + log_prior_list = model.log_prior_list_from(parameter_lists=parameter_lists) + log_posterior_list = np.array([search_internal.log_posterior_list]) + log_likelihood_list = [ + lp - prior for lp, prior in zip(log_posterior_list, log_prior_list) + ] + + weight_list = len(log_likelihood_list) * [1.0] + + sample_list = Sample.from_lists( + model=model, + parameter_lists=parameter_lists, + log_likelihood_list=log_likelihood_list, + log_prior_list=log_prior_list, + weight_list=weight_list, + ) + + samples_info = { + "total_iterations": total_iterations, + "time": self.timer.time if self.timer else None, + } + + return Samples( + model=model, + sample_list=sample_list, + samples_info=samples_info, + ) + + +class BFGS(AbstractBFGS): + """ + The BFGS non-linear search, which wraps the scipy Broyden-Fletcher-Goldfarb-Shanno (BFGS) algorithm. + + For a full description of the scipy BFGS method, checkout its documentation: + + https://docs.scipy.org/doc/scipy/reference/optimize.minimize-bfgs.html#optimize-minimize-bfgs + """ + + method = "BFGS" + + +class LBFGS(AbstractBFGS): + """ + The L-BFGS non-linear search, which wraps the scipy Limited-memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) + algorithm. + + For a full description of the scipy L-BFGS method, checkout its documentation: + + https://docs.scipy.org/doc/scipy/reference/optimize.minimize-lbfgsb.html + """ + + method = "L-BFGS-B" diff --git a/autofit/non_linear/search/mle/drawer/search.py b/autofit/non_linear/search/mle/drawer/search.py index b436cf479..3a359a730 100644 --- a/autofit/non_linear/search/mle/drawer/search.py +++ b/autofit/non_linear/search/mle/drawer/search.py @@ -18,9 +18,11 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + total_draws: int = 50, initializer: Optional[AbstractInitializer] = None, iterations_per_full_update: int = None, iterations_per_quick_update: int = None, + silence: bool = False, session: Optional[sa.orm.Session] = None, **kwargs, ): @@ -64,8 +66,6 @@ def __init__( An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ - number_of_cores = 1 - super().__init__( name=name, path_prefix=path_prefix, @@ -73,11 +73,14 @@ def __init__( initializer=initializer, iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, - number_of_cores=number_of_cores, + number_of_cores=1, + silence=silence, session=session, **kwargs, ) + self.total_draws = total_draws + self.logger.debug("Creating Drawer Search") def _fit(self, model: AbstractPriorModel, analysis): @@ -108,7 +111,7 @@ def _fit(self, model: AbstractPriorModel, analysis): convert_to_chi_squared=False, ) - total_draws = self.config_dict_search["total_draws"] + total_draws = self.total_draws self.logger.info( f"Performing DrawerSearch for a total of {total_draws} points." @@ -119,7 +122,7 @@ def _fit(self, model: AbstractPriorModel, analysis): parameter_lists, log_posterior_list, ) = self.initializer.samples_from_model( - total_points=self.config_dict_search["total_draws"], + total_points=self.total_draws, model=model, fitness=fitness, paths=self.paths, diff --git a/autofit/non_linear/search/nest/abstract_nest.py b/autofit/non_linear/search/nest/abstract_nest.py index eb89ba983..d58d40e27 100644 --- a/autofit/non_linear/search/nest/abstract_nest.py +++ b/autofit/non_linear/search/nest/abstract_nest.py @@ -23,6 +23,7 @@ def __init__( iterations_per_quick_update: Optional[int] = None, iterations_per_full_update: Optional[int] = None, number_of_cores: Optional[int] = None, + silence: bool = False, session: Optional[sa.orm.Session] = None, initializer: Optional[AbstractInitializer] = None, **kwargs @@ -57,14 +58,11 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs ) - @property - def config_type(self): - return conf.instance["non_linear"]["nest"] - @property def samples_cls(self): return SamplesNest diff --git a/autofit/non_linear/search/nest/dynesty/search/abstract.py b/autofit/non_linear/search/nest/dynesty/search/abstract.py index 913bd2b62..de8d27e5e 100644 --- a/autofit/non_linear/search/nest/dynesty/search/abstract.py +++ b/autofit/non_linear/search/nest/dynesty/search/abstract.py @@ -1,3 +1,4 @@ +import logging import os from abc import ABC from typing import Dict, Optional, Tuple, Union @@ -5,7 +6,6 @@ import numpy as np import warnings -from autoconf import conf from autofit import exc from autofit.database.sqlalchemy_ import sa from autofit.non_linear.fitness import Fitness @@ -14,6 +14,9 @@ from autofit.non_linear.search.nest.abstract_nest import AbstractNest from autofit.non_linear.samples.sample import Sample from autofit.non_linear.samples.nest import SamplesNest +from autofit.non_linear.test_mode import is_test_mode + +logger = logging.getLogger(__name__) def prior_transform(cube, model): @@ -33,9 +36,23 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + bound: str = "multi", + sample: str = "auto", + bootstrap: Optional[int] = None, + enlarge: Optional[float] = None, + walks: int = 5, + facc: float = 0.2, + slices: int = 5, + fmove: float = 0.9, + max_move: int = 100, + update_interval: Optional[float] = None, + first_update: Optional[dict] = None, + maxcall: Optional[int] = None, iterations_per_quick_update: int = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, + force_x1_cpu: bool = False, session: Optional[sa.orm.Session] = None, **kwargs, ): @@ -56,21 +73,24 @@ def __init__( unique_tag The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database and also acts as the folder after the path prefix and before the search name. + bound + Method used to approximately bound the prior using the current set of live points. + sample + Method used to sample uniformly within the likelihood constraint. + maxcall + Maximum number of likelihood evaluations. iterations_per_full_update - The number of iterations performed between every Dynesty back-up (via dumping the Dynesty instance as a - pickle) + The number of iterations performed between every Dynesty back-up. number_of_cores The number of cores sampling is performed using a Python multiprocessing Pool instance. + silence + If True, the default print output of the non-linear search is silenced. + force_x1_cpu + If True, force single-CPU mode even when number_of_cores > 1. session An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ - number_of_cores = ( - self._config("parallel", "number_of_cores") - if number_of_cores is None - else number_of_cores - ) - super().__init__( name=name, path_prefix=path_prefix, @@ -78,12 +98,50 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs, ) + self.bound = bound + self.sample = sample + self.bootstrap = bootstrap + self.enlarge = enlarge + self.walks = walks + self.facc = facc + self.slices = slices + self.fmove = fmove + self.max_move = max_move + self.update_interval = update_interval + self.first_update = first_update + + self.maxcall = maxcall + self.force_x1_cpu = force_x1_cpu + self.logger.debug(f"Creating {self.__class__.__name__} Search") + @property + def search_kwargs(self) -> Dict: + """Shared search kwargs passed to both Static and Dynamic Dynesty samplers.""" + return { + "bound": self.bound, + "sample": self.sample, + "bootstrap": self.bootstrap, + "enlarge": self.enlarge, + "walks": self.walks, + "facc": self.facc, + "slices": self.slices, + "fmove": self.fmove, + "max_move": self.max_move, + "update_interval": self.update_interval, + "first_update": self.first_update, + } + + @property + def run_kwargs(self) -> Dict: + """Run kwargs specific to each subclass, excluding maxcall.""" + raise NotImplementedError() + def _fit( self, model: AbstractPriorModel, @@ -141,13 +199,7 @@ def _fit( while not finished: try: - if ( - conf.instance["non_linear"]["nest"][self.__class__.__name__][ - "parallel" - ].get("force_x1_cpu") - or self.kwargs.get("force_x1_cpu") - or analysis._use_jax - ): + if self.force_x1_cpu or analysis._use_jax: raise RuntimeError from dynesty.pool import Pool @@ -284,10 +336,8 @@ def iterations_from( """ if isinstance(self.paths, NullPaths): - maxcall = self.config_dict_run.get("maxcall") - - if maxcall is not None: - return maxcall, maxcall + if self.maxcall is not None: + return self.maxcall, self.maxcall return int(1e99), int(1e99) try: @@ -295,8 +345,8 @@ def iterations_from( except AttributeError: total_iterations = 0 - if self.config_dict_run.get("maxcall") is not None: - iterations = self.config_dict_run["maxcall"] - total_iterations + if self.maxcall is not None: + iterations = self.maxcall - total_iterations return int(iterations), int(total_iterations) return self.iterations_per_full_update, int(total_iterations) @@ -328,12 +378,6 @@ def run_search_internal( search_internal=search_internal ) - config_dict_run = { - key: value - for key, value in self.config_dict_run.items() - if key != "maxcall" - } - if iterations > 0: with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -342,7 +386,7 @@ def run_search_internal( maxcall=iterations, print_progress=not self.silence, checkpoint_file=self.checkpoint_file, - **config_dict_run, + **self.run_kwargs, ) iterations_after_run = np.sum(search_internal.results.ncall) @@ -352,7 +396,7 @@ def run_search_internal( return ( total_iterations == iterations_after_run - or total_iterations == self.config_dict_run.get("maxcall") + or total_iterations == self.maxcall ) def write_uses_pool(self, uses_pool: bool) -> str: @@ -395,29 +439,12 @@ def checkpoint_file(self) -> str: except TypeError: pass - def config_dict_test_mode_from(self, config_dict: Dict) -> Dict: - """ - Returns a configuration dictionary for test mode meaning that the sampler terminates as quickly as possible. - - Entries which set the total number of samples of the sampler (e.g. maximum calls, maximum likelihood - evaluations) are reduced to low values meaning it terminates nearly immediately. - - Parameters - ---------- - config_dict - The original configuration dictionary for this sampler which includes entries controlling how fast the - sampler terminates. - - Returns - ------- - A configuration dictionary where settings which control the sampler's number of samples are reduced so it - terminates as quickly as possible. - """ - return { - **config_dict, - "maxiter": 1, - "maxcall": 1, - } + def apply_test_mode(self): + logger.warning( + "TEST MODE 1 (reduced iterations): Sampler will run with " + "minimal iterations for faster completion." + ) + self.maxcall = 1 def live_points_init_from(self, model, fitness): """ diff --git a/autofit/non_linear/search/nest/dynesty/search/dynamic.py b/autofit/non_linear/search/nest/dynesty/search/dynamic.py index 4da41da66..705bb8a98 100644 --- a/autofit/non_linear/search/nest/dynesty/search/dynamic.py +++ b/autofit/non_linear/search/nest/dynesty/search/dynamic.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Optional +from typing import Dict, Optional from autofit.mapper.prior_model.abstract import AbstractPriorModel @@ -24,9 +24,16 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + nlive_init: int = 500, + dlogz_init: float = 0.01, + logl_max_init: float = float("inf"), + maxcall_init: Optional[int] = None, + maxiter: Optional[int] = None, + maxiter_init: Optional[int] = None, iterations_per_quick_update: int = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, **kwargs ): """ @@ -46,12 +53,14 @@ def __init__( unique_tag The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database and also acts as the folder after the path prefix and before the search name. + nlive_init + Number of live points used during the initial exploration phase. + dlogz_init + Stopping criterion for the initial baseline run. iterations_per_full_update The number of iterations performed between update (e.g. output latest model to hard-disk, visualization). number_of_cores The number of cores sampling is performed using a Python multiprocessing Pool instance. - session - An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ super().__init__( @@ -61,11 +70,34 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, **kwargs ) + self.nlive_init = nlive_init + self.dlogz_init = dlogz_init + self.logl_max_init = logl_max_init + self.maxcall_init = maxcall_init + self.maxiter = maxiter + self.maxiter_init = maxiter_init + + from autofit.non_linear.test_mode import is_test_mode + if is_test_mode(): + self.apply_test_mode() + self.logger.debug("Creating DynestyDynamic Search") + @property + def run_kwargs(self) -> Dict: + return { + "dlogz_init": self.dlogz_init, + "logl_max_init": self.logl_max_init, + "maxcall_init": self.maxcall_init, + "maxiter": self.maxiter, + "maxiter_init": self.maxiter_init, + "nlive_init": self.nlive_init, + } + @property def search_internal(self): from dynesty.dynesty import DynamicNestedSampler @@ -128,7 +160,7 @@ def search_internal_from( ndim=model.prior_count, queue_size=queue_size, pool=pool, - **self.config_dict_search + **self.search_kwargs, ) self.write_uses_pool(uses_pool=False) @@ -139,9 +171,9 @@ def search_internal_from( ndim=model.prior_count, logl_args=[model, fitness], ptform_args=[model], - **self.config_dict_search + **self.search_kwargs, ) @property def number_live_points(self): - return self.config_dict_run["nlive_init"] \ No newline at end of file + return self.nlive_init diff --git a/autofit/non_linear/search/nest/dynesty/search/static.py b/autofit/non_linear/search/nest/dynesty/search/static.py index 87fd1dc1d..09210f5c6 100644 --- a/autofit/non_linear/search/nest/dynesty/search/static.py +++ b/autofit/non_linear/search/nest/dynesty/search/static.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import Optional, Union +from typing import Dict, Optional, Union from autofit.database.sqlalchemy_ import sa @@ -29,9 +29,14 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[Union[str, Path]] = None, unique_tag: Optional[str] = None, + nlive: int = 50, + dlogz: Optional[float] = None, + maxiter: Optional[int] = None, + logl_max: float = float("inf"), iterations_per_quick_update: int = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, session: Optional[sa.orm.Session] = None, **kwargs, ): @@ -52,6 +57,15 @@ def __init__( unique_tag The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database and also acts as the folder after the path prefix and before the search name. + nlive + Number of live points used for sampling. + dlogz + Stopping criterion: iteration stops when the estimated contribution of the remaining prior volume + to the total evidence falls below this threshold. + maxiter + Maximum number of iterations. + logl_max + Maximum log-likelihood value allowed. iterations_per_full_update The number of iterations performed between update (e.g. output latest model to hard-disk, visualization). number_of_cores @@ -67,10 +81,28 @@ def __init__( iterations_per_quick_update=iterations_per_quick_update, iterations_per_full_update=iterations_per_full_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs, ) + self.nlive = nlive + self.dlogz = dlogz + self.maxiter = maxiter + self.logl_max = logl_max + + from autofit.non_linear.test_mode import is_test_mode + if is_test_mode(): + self.apply_test_mode() + + @property + def run_kwargs(self) -> Dict: + return { + "dlogz": self.dlogz, + "maxiter": self.maxiter, + "logl_max": self.logl_max, + } + @property def search_internal(self): from dynesty import NestedSampler as StaticSampler @@ -131,7 +163,8 @@ def search_internal_from( live_points=live_points, queue_size=queue_size, pool=pool, - **self.config_dict_search, + nlive=self.nlive, + **self.search_kwargs, ) self.write_uses_pool(uses_pool=False) @@ -142,9 +175,10 @@ def search_internal_from( logl_args=[model, fitness], ptform_args=[model], live_points=live_points, - **self.config_dict_search, + nlive=self.nlive, + **self.search_kwargs, ) @property def number_live_points(self): - return self.config_dict_search["nlive"] + return self.nlive diff --git a/autofit/non_linear/search/nest/nautilus/search.py b/autofit/non_linear/search/nest/nautilus/search.py index bec9472fa..b9b568b83 100644 --- a/autofit/non_linear/search/nest/nautilus/search.py +++ b/autofit/non_linear/search/nest/nautilus/search.py @@ -6,7 +6,6 @@ from autofit.database.sqlalchemy_ import sa -from autoconf import conf from autofit.mapper.prior_model.abstract import AbstractPriorModel from autofit.mapper.prior.vectorized import PriorVectorized from autofit.non_linear.fitness import Fitness @@ -14,6 +13,7 @@ from autofit.non_linear.search.nest import abstract_nest from autofit.non_linear.samples.sample import Sample from autofit.non_linear.samples.nest import SamplesNest +from autofit.non_linear.test_mode import is_test_mode logger = logging.getLogger(__name__) @@ -37,12 +37,30 @@ def __init__( name: Optional[str] = None, path_prefix: Optional[str] = None, unique_tag: Optional[str] = None, + n_live: int = 3000, + n_update: Optional[int] = None, + enlarge_per_dim: float = 1.1, + n_points_min: Optional[int] = None, + split_threshold: int = 100, + n_networks: int = 4, + n_batch: int = 100, + n_like_new_bound: Optional[int] = None, + vectorized: bool = False, + seed: Optional[int] = None, + f_live: float = 0.01, + n_shell: int = 1, + n_eff: int = 500, + n_like_max: float = float("inf"), + discard_exploration: bool = False, + verbose: bool = True, iterations_per_quick_update: Optional[int] = None, iterations_per_full_update: int = None, - number_of_cores: int = None, + number_of_cores: int = 1, + silence: bool = False, + force_x1_cpu: bool = False, session: Optional[sa.orm.Session] = None, - use_jax_vmap : bool = True, - **kwargs + use_jax_vmap: bool = True, + **kwargs, ): """ A Nautilus non-linear search. @@ -64,20 +82,28 @@ def __init__( unique_tag The name of a unique tag for this model-fit, which will be given a unique entry in the sqlite database and also acts as the folder after the path prefix and before the search name. + n_live + Number of live points used for sampling. + n_batch + Number of likelihood evaluations performed at each step. + n_like_max + Maximum number of likelihood evaluations before stopping. + f_live + Maximum fraction of evidence in the live set before terminating. + n_eff + Minimum effective sample size before stopping. iterations_per_full_update The number of iterations performed between update (e.g. output latest model to hard-disk, visualization). number_of_cores The number of cores sampling is performed using a Python multiprocessing Pool instance. + silence + If True, the default print output of the non-linear search is silenced. + force_x1_cpu + If True, force single-CPU mode even when number_of_cores > 1. session An SQLalchemy session instance so the results of the model-fit are written to an SQLite database. """ - number_of_cores = ( - self._config("parallel", "number_of_cores") - if number_of_cores is None - else number_of_cores - ) - super().__init__( name=name, path_prefix=path_prefix, @@ -85,13 +111,43 @@ def __init__( iterations_per_full_update=iterations_per_full_update, iterations_per_quick_update=iterations_per_quick_update, number_of_cores=number_of_cores, + silence=silence, session=session, **kwargs, ) + self.n_live = n_live + self.n_update = n_update + self.enlarge_per_dim = enlarge_per_dim + self.n_points_min = n_points_min + self.split_threshold = split_threshold + self.n_networks = n_networks + self.n_batch = n_batch + self.n_like_new_bound = n_like_new_bound + self.vectorized = vectorized + self.seed = seed + + self.f_live = f_live + self.n_shell = n_shell + self.n_eff = n_eff + self.n_like_max = n_like_max + self.discard_exploration = discard_exploration + self.verbose = verbose + + self.force_x1_cpu = force_x1_cpu + self.use_jax_vmap = use_jax_vmap + + if is_test_mode(): + self.apply_test_mode() + self.logger.debug("Creating Nautilus Search") - self.use_jax_vmap = use_jax_vmap + def apply_test_mode(self): + logger.warning( + "TEST MODE 1 (reduced iterations): Sampler will run with " + "minimal iterations for faster completion." + ) + self.n_like_max = 1 def _fit(self, model: AbstractPriorModel, analysis): """ @@ -127,11 +183,7 @@ def _fit(self, model: AbstractPriorModel, analysis): "Starting new Nautilus non-linear search (no previous samples found)." ) - if ( - self.config_dict.get("force_x1_cpu") - or self.kwargs.get("force_x1_cpu") - or analysis._use_jax - ): + if self.force_x1_cpu or analysis._use_jax: fitness = Fitness( model=model, @@ -141,7 +193,7 @@ def _fit(self, model: AbstractPriorModel, analysis): resample_figure_of_merit=-1.0e99, iterations_per_quick_update=self.iterations_per_quick_update, use_jax_vmap=self.use_jax_vmap, - batch_size=self.config_dict_search["n_batch"], + batch_size=self.n_batch, ) search_internal = self.fit_x1_cpu( @@ -225,22 +277,22 @@ def fit_x1_cpu(self, fitness, model, analysis): "Running search where parallelization is disabled." ) - config_dict = self.config_dict_search - try: - config_dict.pop("vectorized") - except KeyError: - pass - - vectorized = fitness.use_jax_vmap - search_internal = self.sampler_cls( prior=PriorVectorized(model=model), likelihood=fitness.call_wrap, n_dim=model.prior_count, filepath=self.checkpoint_file, pool=None, - vectorized=vectorized, - **config_dict, + vectorized=fitness.use_jax_vmap, + n_live=self.n_live, + n_update=self.n_update, + enlarge_per_dim=self.enlarge_per_dim, + n_points_min=self.n_points_min, + split_threshold=self.split_threshold, + n_networks=self.n_networks, + n_batch=self.n_batch, + n_like_new_bound=self.n_like_new_bound, + seed=self.seed, ) return self.call_search(search_internal=search_internal, model=model, analysis=analysis, fitness=fitness) @@ -272,7 +324,16 @@ def fit_multiprocessing(self, fitness, model, analysis): n_dim=model.prior_count, filepath=self.checkpoint_file, pool=self.number_of_cores, - **self.config_dict_search, + n_live=self.n_live, + n_update=self.n_update, + enlarge_per_dim=self.enlarge_per_dim, + n_points_min=self.n_points_min, + split_threshold=self.split_threshold, + n_networks=self.n_networks, + n_batch=self.n_batch, + n_like_new_bound=self.n_like_new_bound, + vectorized=self.vectorized, + seed=self.seed, ) search_internal = self.call_search( @@ -310,7 +371,7 @@ def call_search(self, search_internal, model, analysis, fitness): finished = False - minimum_iterations_per_full_updates = 3 * self.config_dict_search["n_live"] + minimum_iterations_per_full_updates = 3 * self.n_live if self.iterations_per_full_update < minimum_iterations_per_full_updates: @@ -334,14 +395,12 @@ def call_search(self, search_internal, model, analysis, fitness): search_internal=search_internal ) - config_dict_run = { - key: value - for key, value in self.config_dict_run.items() - if key != "n_like_max" - } - search_internal.run( - **config_dict_run, + f_live=self.f_live, + n_shell=self.n_shell, + n_eff=self.n_eff, + discard_exploration=self.discard_exploration, + verbose=self.verbose, n_like_max=iterations, ) @@ -349,7 +408,7 @@ def call_search(self, search_internal, model, analysis, fitness): if ( total_iterations == iterations_after_run - or iterations_after_run == self.config_dict_run["n_like_max"] + or iterations_after_run == self.n_like_max ): finished = True @@ -388,10 +447,8 @@ def iterations_from( """ if isinstance(self.paths, NullPaths): - n_like_max = self.config_dict_run.get("n_like_max") - - if n_like_max is not None: - return n_like_max, n_like_max + if self.n_like_max is not None and self.n_like_max != float("inf"): + return int(self.n_like_max), int(self.n_like_max) return int(1e99), int(1e99) try: @@ -401,9 +458,9 @@ def iterations_from( iterations = total_iterations + self.iterations_per_full_update - if self.config_dict_run["n_like_max"] is not None: - if iterations > self.config_dict_run["n_like_max"]: - iterations = self.config_dict_run["n_like_max"] + if self.n_like_max is not None and self.n_like_max != float("inf"): + if iterations > self.n_like_max: + iterations = int(self.n_like_max) return iterations, total_iterations @@ -495,31 +552,4 @@ def samples_via_internal_from( @property def batch_size(self): - return self.config_dict_search.get("n_batch") - - @property - def config_dict(self): - return conf.instance["non_linear"]["nest"][self.__class__.__name__] - - def config_dict_test_mode_from(self, config_dict : Dict) -> Dict: - """ - Returns a configuration dictionary for test mode meaning that the sampler terminates as quickly as possible. - - Entries which set the total number of samples of the sampler (e.g. maximum calls, maximum likelihood - evaluations) are reduced to low values meaning it terminates nearly immediately. - - Parameters - ---------- - config_dict - The original configuration dictionary for this sampler which includes entries controlling how fast the - sampler terminates. - - Returns - ------- - A configuration dictionary where settings which control the sampler's number of samples are reduced so it - terminates as quickly as possible. - """ - return { - **config_dict, - "n_like_max": 1, - } \ No newline at end of file + return self.n_batch \ No newline at end of file diff --git a/test_autofit/aggregator/test_reference.py b/test_autofit/aggregator/test_reference.py index 37af3e935..6d86443b6 100644 --- a/test_autofit/aggregator/test_reference.py +++ b/test_autofit/aggregator/test_reference.py @@ -89,8 +89,8 @@ def test_database_info( assert ( (output_directory / "database.info").read_text() == """ unique_id,name,unique_tag,total_free_parameters,is_complete - d05be1e6380082adea5c918af392d2b9, , , 4, True -d05be1e6380082adea5c918af392d2b9_0, , , 0, -d05be1e6380082adea5c918af392d2b9_1, , , 0, + e97671c16b0b1070fb51af9d229d69cd, , , 4, True +e97671c16b0b1070fb51af9d229d69cd_0, , , 0, +e97671c16b0b1070fb51af9d229d69cd_1, , , 0, """ ) diff --git a/test_autofit/config/non_linear/mcmc.yaml b/test_autofit/config/non_linear/mcmc.yaml deleted file mode 100644 index 0ff3bea43..000000000 --- a/test_autofit/config/non_linear/mcmc.yaml +++ /dev/null @@ -1,47 +0,0 @@ -Emcee: - auto_correlations: - change_threshold: 0.01 - check_for_convergence: true - check_size: 100 - required_length: 50 - initialize: - ball_lower_limit: 0.49 - ball_upper_limit: 0.51 - method: prior - parallel: - force_x1_cpu: true - number_of_cores: 1 - printing: - silence: false - run: - nsteps: 2000 - search: - nwalkers: 50 -Zeus: - auto_correlations: - change_threshold: 0.01 - check_for_convergence: true - check_size: 100 - required_length: 50 - initialize: - ball_lower_limit: 0.49 - ball_upper_limit: 0.51 - method: prior - parallel: - number_of_cores: 1 - printing: - silence: false - run: - check_walkers: true - light_mode: false - maxiter: 10000 - maxsteps: 10000 - mu: 1.0 - nsteps: 2000 - patience: 5 - shuffle_ensemble: true - tolerance: 0.05 - tune: true - vectorize: false - search: - nwalkers: 50 \ No newline at end of file diff --git a/test_autofit/config/non_linear/mle.yaml b/test_autofit/config/non_linear/mle.yaml deleted file mode 100644 index e68dd9763..000000000 --- a/test_autofit/config/non_linear/mle.yaml +++ /dev/null @@ -1,28 +0,0 @@ -Drawer: - initialize: - ball_lower_limit: 0.49 - ball_upper_limit: 0.51 - method: prior - printing: - silence: false - search: - total_draws: 10 -LBFGS: - initialize: - ball_lower_limit: 0.49 - ball_upper_limit: 0.51 - method: prior - options: - disp: false - eps: 1.0e-08 - ftol: 2.220446049250313e-09 - gtol: 1.0e-05 - iprint: -1.0 - maxcor: 10 - maxfun: 15000 - maxiter: 15000 - maxls: 20 - printing: - silence: false - search: - tol: null diff --git a/test_autofit/config/non_linear/mock.yaml b/test_autofit/config/non_linear/mock.yaml deleted file mode 100644 index c34f44bcd..000000000 --- a/test_autofit/config/non_linear/mock.yaml +++ /dev/null @@ -1,11 +0,0 @@ -MockMLE: - initialize: - method: prior - printing: - silence: false -MockSearch: - initialize: - method: prior - printing: - silence: false - search: {} \ No newline at end of file diff --git a/test_autofit/config/non_linear/nest.yaml b/test_autofit/config/non_linear/nest.yaml deleted file mode 100644 index a54542e0d..000000000 --- a/test_autofit/config/non_linear/nest.yaml +++ /dev/null @@ -1,79 +0,0 @@ -DynestyDynamic: - initialize: - method: prior - parallel: - number_of_cores: 4 - printing: - silence: false - run: - dlogz_init: 0.01 - logl_max_init: .inf - maxcall: null - maxcall_init: null - maxiter: null - maxiter_init: null - nlive_init: 5 - search: - bootstrap: 1 - bound: balls - enlarge: 2 - facc: 0.6 - fmove: 0.8 - logl_max: .inf - max_move: 101 - sample: rwalk - slices: 6 - update_interval: 2.0 - walks: 26 -Nautilus: - search: - n_live: 200 - n_update: null - enlarge_per_dim: 1.2 - n_points_min: null - split_threshold: 50 - n_networks: 2 - n_batch: 50 - n_like_new_bound: null - vectorized: false - seed: null - run: - f_live: 0.02 - n_shell: 1 - n_eff: 250 - n_like_max: .inf - discard_exploration: false - verbose: false - initialize: - method: prior - parallel: - number_of_cores: 2 - force_x1_cpu: false - printing: - silence: false -DynestyStatic: - initialize: - method: prior - parallel: - force_x1_cpu: true - number_of_cores: 1 - printing: - silence: true - run: - dlogz: null - logl_max: .inf - maxcall: null - maxiter: null - search: - bootstrap: null - bound: multi - enlarge: null - facc: 0.5 - first_update: null - fmove: 0.9 - max_move: 100 - nlive: 150 - sample: auto - slices: 5 - update_interval: null - walks: 5 \ No newline at end of file diff --git a/test_autofit/database/identifier/test_identifiers.py b/test_autofit/database/identifier/test_identifiers.py index bf0043057..226a35afc 100644 --- a/test_autofit/database/identifier/test_identifiers.py +++ b/test_autofit/database/identifier/test_identifiers.py @@ -390,7 +390,7 @@ def test_dynesty_static(): assert Identifier(af.DynestyStatic()).hash_list == [ "DynestyStatic", "nlive", - "150", + "50", "bound", "multi", "sample", @@ -400,7 +400,7 @@ def test_dynesty_static(): "walks", "5", "facc", - "0.5", + "0.2", "slices", "5", "fmove", diff --git a/test_autofit/graphical/regression/test_static.py b/test_autofit/graphical/regression/test_static.py index f250e669a..7052c8716 100644 --- a/test_autofit/graphical/regression/test_static.py +++ b/test_autofit/graphical/regression/test_static.py @@ -22,10 +22,6 @@ def fit( def _fit(self, model, analysis): pass - @property - def config_type(self): - pass - @property def samples_cls(self): pass diff --git a/test_autofit/interpolator/test_covariance.py b/test_autofit/interpolator/test_covariance.py index 9389e46de..6a1bfc5ec 100644 --- a/test_autofit/interpolator/test_covariance.py +++ b/test_autofit/interpolator/test_covariance.py @@ -1,13 +1,14 @@ import logging +from unittest.mock import patch import pytest import scipy -from autoconf.conf import with_config import numpy as np from autofit import CovarianceInterpolator import autofit as af +from autofit.non_linear.search.nest.dynesty.search.static import DynestyStatic @pytest.fixture(autouse=True) @@ -35,18 +36,24 @@ def test_covariance_matrix(interpolator): ) -def maxcall(func): - return with_config( - "non_linear", - "nest", - "DynestyStatic", - "run", - "maxcall", - value=1, - )(func) +def _maxcall_dynesty(*args, **kwargs): + """Create a DynestyStatic with maxcall=1 for fast test execution.""" + kwargs.setdefault("maxcall", 1) + return DynestyStatic.__new_orig__(*args, **kwargs) + + +@pytest.fixture(autouse=True) +def limit_maxcall(monkeypatch): + """Limit DynestyStatic to maxcall=1 so interpolator tests run fast.""" + original_init = DynestyStatic.__init__ + + def patched_init(self, *args, **kwargs): + kwargs.setdefault("maxcall", 1) + original_init(self, *args, **kwargs) + + monkeypatch.setattr(DynestyStatic, "__init__", patched_init) -@maxcall def test_interpolate(interpolator): try: assert isinstance(interpolator[interpolator.t == 0.5].gaussian.centre, float) @@ -54,7 +61,6 @@ def test_interpolate(interpolator): logging.warning(e) -@maxcall def test_relationships(interpolator): try: relationships = interpolator.relationships(interpolator.t) @@ -63,7 +69,6 @@ def test_relationships(interpolator): logging.warning(e) -@maxcall def test_interpolate_other_field(interpolator): try: assert isinstance( @@ -88,7 +93,6 @@ def test_model(interpolator): assert model.prior_count == 6 -@maxcall def test_single_variable(): samples_list = [ af.SamplesPDF( @@ -115,7 +119,6 @@ def test_single_variable(): assert interpolator[interpolator.t == 25.0].v == pytest.approx(25.0, abs=2.0) -@maxcall def test_variable_and_constant(): samples_list = [ af.SamplesPDF( diff --git a/test_autofit/non_linear/search/mcmc/test_emcee.py b/test_autofit/non_linear/search/mcmc/test_emcee.py index 44484dbb5..e9a413857 100644 --- a/test_autofit/non_linear/search/mcmc/test_emcee.py +++ b/test_autofit/non_linear/search/mcmc/test_emcee.py @@ -1,46 +1,43 @@ -from os import path - -import pytest - -import autofit as af - -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") - - -def test__config__loads_from_file_correctly(): - - search = af.Emcee( - nwalkers=51, - nsteps=2001, - initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), - auto_correlation_settings=af.AutoCorrelationsSettings( - check_for_convergence=False, - check_size=101, - required_length=51, - change_threshold=0.02 - ), - number_of_cores=2, - ) - - assert search.config_dict_search["nwalkers"] == 51 - assert search.config_dict_run["nsteps"] == 2001 - assert isinstance(search.initializer, af.InitializerBall) - assert search.initializer.lower_limit == 0.2 - assert search.initializer.upper_limit == 0.8 - assert search.auto_correlation_settings.check_for_convergence is False - assert search.auto_correlation_settings.check_size == 101 - assert search.auto_correlation_settings.required_length == 51 - assert search.auto_correlation_settings.change_threshold == 0.02 - assert search.number_of_cores == 2 - - search = af.Emcee() - - assert search.config_dict_search["nwalkers"] == 50 - assert search.config_dict_run["nsteps"] == 2000 - assert isinstance(search.initializer, af.InitializerPrior) - assert search.auto_correlation_settings.check_for_convergence is True - assert search.auto_correlation_settings.check_size == 100 - assert search.auto_correlation_settings.required_length == 50 - assert search.auto_correlation_settings.change_threshold == 0.01 - assert search.number_of_cores == 1 - +import pytest + +import autofit as af + +pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") + + +def test__explicit_params(): + + search = af.Emcee( + nwalkers=51, + nsteps=2001, + initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), + auto_correlation_settings=af.AutoCorrelationsSettings( + check_for_convergence=False, + check_size=101, + required_length=51, + change_threshold=0.02 + ), + number_of_cores=2, + ) + + assert search.nwalkers == 51 + assert search.nsteps == 2001 + assert isinstance(search.initializer, af.InitializerBall) + assert search.initializer.lower_limit == 0.2 + assert search.initializer.upper_limit == 0.8 + assert search.auto_correlation_settings.check_for_convergence is False + assert search.auto_correlation_settings.check_size == 101 + assert search.auto_correlation_settings.required_length == 51 + assert search.auto_correlation_settings.change_threshold == 0.02 + assert search.number_of_cores == 2 + + search = af.Emcee() + + assert search.nwalkers == 50 + assert search.nsteps == 2000 + assert isinstance(search.initializer, af.InitializerBall) + assert search.auto_correlation_settings.check_for_convergence is True + assert search.auto_correlation_settings.check_size == 100 + assert search.auto_correlation_settings.required_length == 50 + assert search.auto_correlation_settings.change_threshold == 0.01 + assert search.number_of_cores == 1 diff --git a/test_autofit/non_linear/search/mcmc/test_zeus.py b/test_autofit/non_linear/search/mcmc/test_zeus.py index a5153bf5d..2c8593423 100644 --- a/test_autofit/non_linear/search/mcmc/test_zeus.py +++ b/test_autofit/non_linear/search/mcmc/test_zeus.py @@ -1,46 +1,45 @@ -import pytest -import autofit as af - -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") - - -def test__loads_from_config_file_correct(): - - search = af.Zeus( - nwalkers=51, - nsteps=2001, - initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), - auto_correlation_settings=af.AutoCorrelationsSettings( - check_for_convergence=False, - check_size=101, - required_length=51, - change_threshold=0.02 - ), - tune=False, - number_of_cores=2, - ) - - assert search.config_dict_search["nwalkers"] == 51 - assert search.config_dict_run["nsteps"] == 2001 - assert search.config_dict_run["tune"] == False - assert isinstance(search.initializer, af.InitializerBall) - assert search.initializer.lower_limit == 0.2 - assert search.initializer.upper_limit == 0.8 - assert search.auto_correlation_settings.check_for_convergence is False - assert search.auto_correlation_settings.check_size == 101 - assert search.auto_correlation_settings.required_length == 51 - assert search.auto_correlation_settings.change_threshold == 0.02 - assert search.number_of_cores == 2 - - search = af.Zeus() - - assert search.config_dict_search["nwalkers"] == 50 - assert search.config_dict_run["nsteps"] == 2000 - assert search.config_dict_run["tune"] == True - assert isinstance(search.initializer, af.InitializerPrior) - assert search.auto_correlation_settings.check_for_convergence is True - assert search.auto_correlation_settings.check_size == 100 - assert search.auto_correlation_settings.required_length == 50 - assert search.auto_correlation_settings.change_threshold == 0.01 - assert search.number_of_cores == 1 - +import pytest +import autofit as af + +pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") + + +def test__explicit_params(): + + search = af.Zeus( + nwalkers=51, + nsteps=2001, + initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), + auto_correlation_settings=af.AutoCorrelationsSettings( + check_for_convergence=False, + check_size=101, + required_length=51, + change_threshold=0.02 + ), + tune=False, + number_of_cores=2, + ) + + assert search.nwalkers == 51 + assert search.nsteps == 2001 + assert search.tune is False + assert isinstance(search.initializer, af.InitializerBall) + assert search.initializer.lower_limit == 0.2 + assert search.initializer.upper_limit == 0.8 + assert search.auto_correlation_settings.check_for_convergence is False + assert search.auto_correlation_settings.check_size == 101 + assert search.auto_correlation_settings.required_length == 51 + assert search.auto_correlation_settings.change_threshold == 0.02 + assert search.number_of_cores == 2 + + search = af.Zeus() + + assert search.nwalkers == 50 + assert search.nsteps == 2000 + assert search.tune is True + assert isinstance(search.initializer, af.InitializerBall) + assert search.auto_correlation_settings.check_for_convergence is True + assert search.auto_correlation_settings.check_size == 100 + assert search.auto_correlation_settings.required_length == 50 + assert search.auto_correlation_settings.change_threshold == 0.01 + assert search.number_of_cores == 1 diff --git a/test_autofit/non_linear/search/nest/test_dynesty.py b/test_autofit/non_linear/search/nest/test_dynesty.py index d0f9a58da..4b62c8d25 100644 --- a/test_autofit/non_linear/search/nest/test_dynesty.py +++ b/test_autofit/non_linear/search/nest/test_dynesty.py @@ -1,64 +1,61 @@ -import pytest - -import autofit as af - -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") - -class MockDynestyResults: - def __init__(self, samples, logl, logwt, ncall, logz, nlive): - self.samples = samples - self.logl = logl - self.logwt = logwt - self.ncall = ncall - self.logz = logz - self.nlive = nlive - - -class MockDynestySampler: - def __init__(self, results): - self.results = results - - -def test__loads_from_config_file_if_not_input(): - - search = af.DynestyStatic( - nlive=151, - dlogz=0.1, - iterations_per_full_update=501, - number_of_cores=2, - ) - - assert search.iterations_per_full_update == 501 - - assert search.config_dict_search["nlive"] == 151 - assert search.config_dict_run["dlogz"] == 0.1 - assert search.number_of_cores == 2 - - search = af.DynestyStatic() - - assert search.iterations_per_full_update == 1e99 - - assert search.config_dict_search["nlive"] == 150 - assert search.config_dict_run["dlogz"] == None - assert search.number_of_cores == 1 - - search = af.DynestyDynamic( - facc=0.4, - iterations_per_full_update=501, - dlogz_init=0.2, - number_of_cores=3, - ) - - assert search.iterations_per_full_update == 501 - - assert search.config_dict_search["facc"] == 0.4 - assert search.config_dict_run["dlogz_init"] == 0.2 - assert search.number_of_cores == 3 - - search = af.DynestyDynamic() - - assert search.iterations_per_full_update == 1e99 - - assert search.config_dict_search["facc"] == 0.6 - assert search.config_dict_run["dlogz_init"] == 0.01 - assert search.number_of_cores == 4 \ No newline at end of file +import pytest + +import autofit as af + +pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") + + +class MockDynestyResults: + def __init__(self, samples, logl, logwt, ncall, logz, nlive): + self.samples = samples + self.logl = logl + self.logwt = logwt + self.ncall = ncall + self.logz = logz + self.nlive = nlive + + +class MockDynestySampler: + def __init__(self, results): + self.results = results + + +def test__explicit_params(): + + search = af.DynestyStatic( + nlive=151, + dlogz=0.1, + iterations_per_full_update=501, + number_of_cores=2, + ) + + assert search.iterations_per_full_update == 501 + + assert search.nlive == 151 + assert search.dlogz == 0.1 + assert search.number_of_cores == 2 + + search = af.DynestyStatic() + + assert search.nlive == 50 + assert search.dlogz is None + assert search.number_of_cores == 1 + + search = af.DynestyDynamic( + facc=0.4, + iterations_per_full_update=501, + dlogz_init=0.2, + number_of_cores=3, + ) + + assert search.iterations_per_full_update == 501 + + assert search.facc == 0.4 + assert search.dlogz_init == 0.2 + assert search.number_of_cores == 3 + + search = af.DynestyDynamic() + + assert search.facc == 0.2 + assert search.dlogz_init == 0.01 + assert search.number_of_cores == 1 diff --git a/test_autofit/non_linear/search/nest/test_nautilus.py b/test_autofit/non_linear/search/nest/test_nautilus.py index a76f0454d..5ac5ff357 100644 --- a/test_autofit/non_linear/search/nest/test_nautilus.py +++ b/test_autofit/non_linear/search/nest/test_nautilus.py @@ -5,7 +5,7 @@ pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") -def test__loads_from_config_file_if_not_input(): +def test__explicit_params(): search = af.Nautilus( n_live=500, n_batch=200, @@ -17,27 +17,25 @@ def test__loads_from_config_file_if_not_input(): assert search.iterations_per_full_update == 501 - assert search.config_dict_search["n_live"] == 500 - assert search.config_dict_search["n_batch"] == 200 - assert search.config_dict_run["f_live"] == 0.05 - assert search.config_dict_run["n_eff"] == 1000 + assert search.n_live == 500 + assert search.n_batch == 200 + assert search.f_live == 0.05 + assert search.n_eff == 1000 assert search.number_of_cores == 4 search = af.Nautilus() - assert search.iterations_per_full_update == 1e99 - - assert search.config_dict_search["n_live"] == 200 - assert search.config_dict_search["n_batch"] == 50 - assert search.config_dict_search["enlarge_per_dim"] == 1.2 - assert search.config_dict_search["split_threshold"] == 50 - assert search.config_dict_search["n_networks"] == 2 - assert search.config_dict_search["vectorized"] == False - assert search.config_dict_run["f_live"] == 0.02 - assert search.config_dict_run["n_shell"] == 1 - assert search.config_dict_run["n_eff"] == 250 - assert search.config_dict_run["discard_exploration"] == False - assert search.number_of_cores == 2 + assert search.n_live == 3000 + assert search.n_batch == 100 + assert search.enlarge_per_dim == 1.1 + assert search.split_threshold == 100 + assert search.n_networks == 4 + assert search.vectorized is False + assert search.f_live == 0.01 + assert search.n_shell == 1 + assert search.n_eff == 500 + assert search.discard_exploration is False + assert search.number_of_cores == 1 def test__identifier_fields(): @@ -48,10 +46,8 @@ def test__identifier_fields(): assert "n_shell" in search.__identifier_fields__ -def test__config_dict_test_mode(): +def test__test_mode(): search = af.Nautilus() + search.apply_test_mode() - config_dict = {"n_like_max": float("inf"), "f_live": 0.01, "n_eff": 500} - test_config = search.config_dict_test_mode_from(config_dict) - - assert test_config["n_like_max"] == 1 + assert search.n_like_max == 1 diff --git a/test_autofit/non_linear/search/optimize/test_drawer.py b/test_autofit/non_linear/search/optimize/test_drawer.py index d943faa42..ad6d2c352 100644 --- a/test_autofit/non_linear/search/optimize/test_drawer.py +++ b/test_autofit/non_linear/search/optimize/test_drawer.py @@ -1,23 +1,23 @@ -import pytest - -import autofit as af - -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") - - -def test__loads_from_config_file_correct(): - search = af.Drawer( - total_draws=5, - initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), - ) - - assert search.config_dict_search["total_draws"] == 5 - assert isinstance(search.initializer, af.InitializerBall) - assert search.initializer.lower_limit == 0.2 - assert search.initializer.upper_limit == 0.8 - assert search.number_of_cores == 1 - - search = af.Drawer() - - assert search.config_dict_search["total_draws"] == 10 - assert isinstance(search.initializer, af.InitializerPrior) +import pytest + +import autofit as af + +pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") + + +def test__explicit_params(): + search = af.Drawer( + total_draws=5, + initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), + ) + + assert search.total_draws == 5 + assert isinstance(search.initializer, af.InitializerBall) + assert search.initializer.lower_limit == 0.2 + assert search.initializer.upper_limit == 0.8 + assert search.number_of_cores == 1 + + search = af.Drawer() + + assert search.total_draws == 50 + assert isinstance(search.initializer, af.InitializerBall) diff --git a/test_autofit/non_linear/search/optimize/test_lbfgs.py b/test_autofit/non_linear/search/optimize/test_lbfgs.py index 3e981489d..e7b77950b 100644 --- a/test_autofit/non_linear/search/optimize/test_lbfgs.py +++ b/test_autofit/non_linear/search/optimize/test_lbfgs.py @@ -1,55 +1,52 @@ -import pytest - -import autofit as af - -pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") - -def test__loads_from_config_file_correct(): - search = af.LBFGS( - tol=0.2, - disp=True, - maxcor=11, - ftol=2., - gtol=3., - eps=4., - maxfun=25000, - maxiter=26000, - iprint=-2, - maxls=21, - initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), - iterations_per_full_update=10, - number_of_cores=2, - ) - - assert search.config_dict_search["tol"] == 0.2 - assert search.config_dict_options["maxcor"] == 11 - assert search.config_dict_options["ftol"] == 2. - assert search.config_dict_options["gtol"] == 3. - assert search.config_dict_options["eps"] == 4. - assert search.config_dict_options["maxfun"] == 25000 - assert search.config_dict_options["maxiter"] == 26000 - assert search.config_dict_options["iprint"] == -2 - assert search.config_dict_options["maxls"] == 21 - assert search.config_dict_options["disp"] == True - assert isinstance(search.initializer, af.InitializerBall) - assert search.initializer.lower_limit == 0.2 - assert search.initializer.upper_limit == 0.8 - assert search.iterations_per_full_update == 10 - assert search.number_of_cores == 2 - - search = af.LBFGS() - - assert search.config_dict_search["tol"] == None - assert search.config_dict_options["maxcor"] == 10 - assert search.config_dict_options["ftol"] == 2.220446049250313e-09 - assert search.config_dict_options["gtol"] == 1e-05 - assert search.config_dict_options["eps"] == 1e-08 - assert search.config_dict_options["maxfun"] == 15000 - assert search.config_dict_options["maxiter"] == 15000 - assert search.config_dict_options["iprint"] == -1 - assert search.config_dict_options["maxls"] == 20 - assert search.config_dict_options["maxiter"] == 15000 - assert search.config_dict_options["disp"] == False - assert isinstance(search.initializer, af.InitializerPrior) - assert search.iterations_per_full_update == 1e99 - +import pytest + +import autofit as af + +pytestmark = pytest.mark.filterwarnings("ignore::FutureWarning") + +def test__explicit_params(): + search = af.LBFGS( + tol=0.2, + disp=True, + maxcor=11, + ftol=2., + gtol=3., + eps=4., + maxfun=25000, + maxiter=26000, + iprint=-2, + maxls=21, + initializer=af.InitializerBall(lower_limit=0.2, upper_limit=0.8), + iterations_per_full_update=10, + number_of_cores=2, + ) + + assert search.tol == 0.2 + assert search.options["maxcor"] == 11 + assert search.options["ftol"] == 2. + assert search.options["gtol"] == 3. + assert search.options["eps"] == 4. + assert search.options["maxfun"] == 25000 + assert search.options["maxiter"] == 26000 + assert search.options["iprint"] == -2 + assert search.options["maxls"] == 21 + assert search.options["disp"] is True + assert isinstance(search.initializer, af.InitializerBall) + assert search.initializer.lower_limit == 0.2 + assert search.initializer.upper_limit == 0.8 + assert search.iterations_per_full_update == 10 + assert search.number_of_cores == 2 + + search = af.LBFGS() + + assert search.tol is None + assert search.options["maxcor"] == 10 + assert search.options["ftol"] == 2.220446049250313e-09 + assert search.options["gtol"] == 1e-05 + assert search.options["eps"] == 1e-08 + assert search.options["maxfun"] == 15000 + assert search.options["maxiter"] == 15000 + assert search.options["iprint"] == -1 + assert search.options["maxls"] == 20 + assert search.options["disp"] is False + assert isinstance(search.initializer, af.InitializerBall) diff --git a/test_autofit/non_linear/search/test_abstract_search.py b/test_autofit/non_linear/search/test_abstract_search.py index a7d1f8929..4d1678b7b 100644 --- a/test_autofit/non_linear/search/test_abstract_search.py +++ b/test_autofit/non_linear/search/test_abstract_search.py @@ -81,13 +81,13 @@ def test_raises(self, result): class TestSearchConfig: - def test__config_dict_search_accessible(self): + def test__explicit_params_accessible(self): search = af.DynestyStatic(nlive=100) - assert search.config_dict_search["nlive"] == 100 + assert search.nlive == 100 - def test__config_dict_run_accessible(self): + def test__run_params_accessible(self): search = af.DynestyStatic(dlogz=0.5) - assert search.config_dict_run["dlogz"] == 0.5 + assert search.dlogz == 0.5 def test__unique_tag(self): search = af.DynestyStatic(unique_tag="my_tag") diff --git a/test_autofit/non_linear/test_dict.py b/test_autofit/non_linear/test_dict.py index e4c19d5a1..5a8034685 100644 --- a/test_autofit/non_linear/test_dict.py +++ b/test_autofit/non_linear/test_dict.py @@ -10,9 +10,12 @@ def make_dynesty_dict(): "arguments": { "bootstrap": None, "bound": "multi", + "dlogz": None, "enlarge": None, - "facc": 0.5, + "facc": 0.2, + "first_update": None, "fmove": 0.9, + "force_x1_cpu": False, "initial_values": {"arguments": {}, "type": "dict"}, "initializer": { "arguments": {}, @@ -22,9 +25,12 @@ def make_dynesty_dict(): "inplace": False, "iterations_per_full_update": 1e99, "iterations_per_quick_update": 1e99, + "logl_max": float("inf"), "max_move": 100, + "maxcall": None, + "maxiter": None, "name": "", - "nlive": 150, + "nlive": 50, "number_of_cores": 1, "path_prefix": None, "paths": { @@ -33,8 +39,10 @@ def make_dynesty_dict(): "type": "instance", }, "sample": "auto", + "silence": False, "slices": 5, "unique_tag": None, + "update_interval": None, "walks": 5, }, "class_path": "autofit.non_linear.search.nest.dynesty.search.static.DynestyStatic",