diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 8a3f6aecb..a744f84bf 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -88,7 +88,7 @@ jobs: mv libensemble/tests/.cov* . - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -98,4 +98,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: crate-ci/typos@v1.47.1 + - uses: crate-ci/typos@v1.47.2 diff --git a/.github/workflows/extra.yml b/.github/workflows/extra.yml index 16a4213ad..f3de20e98 100644 --- a/.github/workflows/extra.yml +++ b/.github/workflows/extra.yml @@ -101,7 +101,7 @@ jobs: mv libensemble/tests/.cov* . - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -111,4 +111,4 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: crate-ci/typos@v1.47.1 + - uses: crate-ci/typos@v1.47.2 diff --git a/docs/examples/calling_scripts.rst b/docs/examples/calling_scripts.rst index 7a58ad05e..269fe1d3c 100644 --- a/docs/examples/calling_scripts.rst +++ b/docs/examples/calling_scripts.rst @@ -47,6 +47,6 @@ paired with a gest-api ``simulator`` callable. :language: python :caption: tests/regression_tests/test_asktell_aposmm_nlopt.py :linenos: - :end-at: workflow.exit_criteria = ExitCriteria(sim_max=2000, wallclock_max=600) + :end-at: H, _, _ = workflow.run(sim_max=3000, wallclock_max=600) .. _regression tests: https://github.com/Libensemble/libensemble/tree/develop/libensemble/tests/regression_tests diff --git a/docs/platforms/aurora.rst b/docs/platforms/aurora.rst index c29ed0bc0..a2f652f3a 100644 --- a/docs/platforms/aurora.rst +++ b/docs/platforms/aurora.rst @@ -56,8 +56,8 @@ simulations for each worker: .. code-block:: python - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=nsim_workers * 2) + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=nsim_workers * 2) Now grab an interactive session on two nodes (or use the batch script at ``../submission_scripts/submit_pbs_aurora.sh``):: diff --git a/docs/tutorials/aposmm_tutorial.rst b/docs/tutorials/aposmm_tutorial.rst index 7cc0d7b9f..094a7c1f6 100644 --- a/docs/tutorials/aposmm_tutorial.rst +++ b/docs/tutorials/aposmm_tutorial.rst @@ -105,7 +105,7 @@ libEnsemble classes, APOSMM, and our simulator callable: from libensemble import Ensemble from libensemble.gen_classes import APOSMM from gest_api.vocs import VOCS - from libensemble.specs import SimSpecs, GenSpecs, ExitCriteria + from libensemble.specs import SimSpecs, GenSpecs APOSMM supports a wide variety of external optimizers. The ``rc.aposmm_optimizers`` statement above indicates to APOSMM which optimization method package to use, @@ -156,9 +156,8 @@ Finally, we configure the simulation function, exit criteria, and run the workfl :linenos: workflow.sim_specs = SimSpecs(simulator=six_hump_camel_func, vocs=vocs) - workflow.exit_criteria = ExitCriteria(sim_max=2000) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=2000) if workflow.is_manager: # We can map our variables back to an array for easy printing diff --git a/docs/tutorials/gpcam_tutorial.rst b/docs/tutorials/gpcam_tutorial.rst index 190697374..959ef22f2 100644 --- a/docs/tutorials/gpcam_tutorial.rst +++ b/docs/tutorials/gpcam_tutorial.rst @@ -210,7 +210,7 @@ If you wish to make your own functions based on the above, those can be imported from pprint import pprint from libensemble import Ensemble - from libensemble.specs import LibeSpecs, GenSpecs, SimSpecs, AllocSpecs, ExitCriteria + from libensemble.specs import LibeSpecs, GenSpecs, SimSpecs, AllocSpecs # If importing from libensemble from libensemble.gen_funcs.persistent_gpCAM import persistent_gpCAM @@ -256,15 +256,12 @@ If you wish to make your own functions based on the above, those can be imported user={"async_return": False}, # False = batch returns ) - exit_criteria = ExitCriteria(sim_max=num_batches * batch_size) - - # Initialize and run the ensemble. + # Initialize the ensemble. ensemble = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, alloc_specs=alloc_specs, - exit_criteria=exit_criteria, ) At the end of our calling script we run the ensemble. @@ -275,7 +272,7 @@ At the end of our calling script we run the ensemble. cleanup() ensemble.persis_info = {} - H, persis_info, flag = ensemble.run() # Start the ensemble. Blocks until completion. + H, persis_info, flag = ensemble.run(sim_max=num_batches * batch_size) # Start the ensemble. Blocks until completion. ensemble.save_output("H_array", append_attrs=False) # Save H (history of all evaluated points) to file pprint(H[["sim_id", "x", "f"]][:16]) # See first 16 results @@ -293,7 +290,7 @@ To see how the accuracy of the surrogate model improves, we can use previously e cleanup() ensemble.persis_info = {} - H, persis_info, flag = ensemble.run() + H, persis_info, flag = ensemble.run(sim_max=num_batches * batch_size) print(persis_info) Viewing model progression diff --git a/docs/tutorials/xopt_bayesian_gen.rst b/docs/tutorials/xopt_bayesian_gen.rst index 9227ac8ce..bbdf756d9 100644 --- a/docs/tutorials/xopt_bayesian_gen.rst +++ b/docs/tutorials/xopt_bayesian_gen.rst @@ -25,7 +25,7 @@ Imports from libensemble import Ensemble from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f - from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs Simulator Function ------------------ @@ -93,17 +93,15 @@ The simulator is a simple callable function that takes a dictionary of inputs an ) alloc_specs = AllocSpecs(alloc_f=alloc_f) - exit_criteria = ExitCriteria(sim_max=12) workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, alloc_specs=alloc_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=12) if workflow.is_manager: print(f"Completed {len(H)} simulations") @@ -158,10 +156,9 @@ Reset generator and change to libEnsemble-style simulator: sim_specs=sim_specs, alloc_specs=alloc_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=12) if workflow.is_manager: print(f"Completed {len(H)} simulations") diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index 24b47d72b..63365cbc2 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -1,7 +1,9 @@ import logging +import warnings import numpy.typing as npt +from libensemble._deprecation import LibEnsembleDeprecationWarning from libensemble.executors import Executor from libensemble.libE import libE from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs @@ -19,6 +21,13 @@ OVERWRITE_COMMS_WARN = "Cannot reset 'comms' if 'ensemble.libE_specs.comms' is already set." CHANGED_COMMS_WARN = "New 'comms' method detected following initialization of Ensemble. Exiting." +EXIT_CRITERIA_DEPRECATION = ( + "ExitCriteria as a standalone parameter is deprecated as of libEnsemble 2.0 " + "and will be removed in 2.1. Pass exit criteria directly to run() instead: " + "ensemble.run(sim_max=100) or ensemble.run(sim_max=100, wallclock_max=3600). " + "See https://libensemble.readthedocs.io/... for migration guidance." +) + CORRESPONDING_CLASSES = { "sim_specs": SimSpecs, "gen_specs": GenSpecs, @@ -44,7 +53,7 @@ class Ensemble: from libensemble import Ensemble from libensemble.gen_classes.sampling import UniformSample from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + from libensemble.specs import GenSpecs, SimSpecs sampling = Ensemble(parse_args=True) @@ -66,10 +75,8 @@ class Ensemble: batch_size=50, ) - sampling.exit_criteria = ExitCriteria(sim_max=100) - if __name__ == "__main__": - sampling.run() + sampling.run(sim_max=100) sampling.save_output(__file__) Configure by: @@ -159,7 +166,7 @@ def __init__( self, sim_specs: SimSpecs = SimSpecs(), gen_specs: GenSpecs = GenSpecs(), - exit_criteria: ExitCriteria = ExitCriteria(), + exit_criteria: ExitCriteria | None = None, libE_specs: LibeSpecs = LibeSpecs(), alloc_specs: AllocSpecs = AllocSpecs(), persis_info: dict = {}, @@ -169,7 +176,11 @@ def __init__( ): self.sim_specs = sim_specs self.gen_specs = gen_specs - self.exit_criteria = exit_criteria + self._exit_criteria = ExitCriteria() + if exit_criteria is not None: + if isinstance(exit_criteria, ExitCriteria): + warnings.warn(EXIT_CRITERIA_DEPRECATION, LibEnsembleDeprecationWarning, stacklevel=2) + self._exit_criteria = exit_criteria self._libE_specs: LibeSpecs = libE_specs self.alloc_specs = alloc_specs self.persis_info = persis_info @@ -180,6 +191,7 @@ def __init__( self.is_manager = False self.parsed = False self._known_comms: str = "" + self._has_run_n_evals = False if parse_args: self._parse_args() @@ -254,7 +266,9 @@ def ready(self) -> tuple[bool, list[str]]: ): issues.append( "exit_criteria has no stop condition: set at least one of " - "'sim_max', 'gen_max', 'wallclock_max', or 'stop_val'." + "'sim_max', 'gen_max', 'wallclock_max', or 'stop_val' " + "either on an ExitCriteria object or directly via " + "ensemble.run(sim_max=..., gen_max=..., ...)." ) # --- workers: must be determinable --- @@ -308,13 +322,61 @@ def libE_specs(self, new_specs): self._libE_specs.__dict__.update(**new_specs) + @property + def exit_criteria(self) -> ExitCriteria: + return self._exit_criteria + + @exit_criteria.setter + def exit_criteria(self, value: ExitCriteria | None): + if isinstance(value, ExitCriteria): + warnings.warn(EXIT_CRITERIA_DEPRECATION, LibEnsembleDeprecationWarning, stacklevel=2) + self._exit_criteria = value or ExitCriteria() + + def reset(self) -> None: + """Reset the ensemble state to allow a fresh, independent run. + + Clears the accumulated history (``H0``) and ``persis_info`` so that + the next :meth:`run` call starts from a clean slate — as if no + previous run had occurred. + + Use this between two calls to :meth:`run` when you want **independent** + runs rather than the default history-chaining behaviour:: + + ens.run(sim_max=10) # first independent run + ens.reset() # clear accumulated history + ens.run(sim_max=20) # second independent run, H0 is empty again + """ + self.H0 = None + self.persis_info = {} + def _refresh_executor(self): Executor.executor = self.executor or Executor.executor - def run(self) -> tuple[npt.NDArray, dict, int]: + def run( + self, + sim_max: int | None = None, + gen_max: int | None = None, + wallclock_max: float | None = None, + stop_val: tuple[str, float] | None = None, + ) -> tuple[npt.NDArray, dict, int]: """ Initializes libEnsemble. + Parameters + ---------- + sim_max: int, Optional + Maximum number of new simulation evaluations for this run. + Overrides ``exit_criteria.sim_max`` for this call only. + gen_max: int, Optional + Maximum number of new generator calls for this run. + Overrides ``exit_criteria.gen_max`` for this call only. + wallclock_max: float, Optional + Wallclock timeout in seconds for this run. + Overrides ``exit_criteria.wallclock_max`` for this call only. + stop_val: tuple[str, float], Optional + Stop criterion ``(field, value)`` for this run. + Overrides ``exit_criteria.stop_val`` for this call only. + .. dropdown:: MPI/comms Notes Manager--worker intercommunications are parsed from the ``comms`` key of @@ -325,6 +387,25 @@ def run(self) -> tuple[npt.NDArray, dict, int]: will initiate on a **duplicate** of that communicator. Otherwise, a duplicate of ``COMM_WORLD`` will be used. + .. dropdown:: Substeps / multi-step usage + + Pass exit-criteria kwargs to run a subset of an ensemble at a time. + The ensemble history (``H0``) is automatically chained across calls:: + + sampling = Ensemble(...) + sampling.sim_specs = SimSpecs(...) + sampling.gen_specs = GenSpecs(...) + + # Run in three substeps + sampling.run(sim_max=30) + # ... adjust generator hyperparameters ... + sampling.run(sim_max=30) + sampling.run(sim_max=40) + + When ``sim_max`` is used (from kwargs or ``exit_criteria``), + ``libE_specs.final_gen_send`` and ``libE_specs.reuse_output_dir`` are + automatically set to ``True`` to support persistent generators across runs. + Returns ------- @@ -355,16 +436,41 @@ def run(self) -> tuple[npt.NDArray, dict, int]: raise ValueError(CHANGED_COMMS_WARN) assert self._libE_specs is not None + + # Merge kwargs into effective exit criteria for this run + run_kwargs = { + k: v + for k, v in { + "sim_max": sim_max, + "gen_max": gen_max, + "wallclock_max": wallclock_max, + "stop_val": stop_val, + }.items() + if v is not None + } + if run_kwargs: + effective_exit = self._exit_criteria.model_copy(update=run_kwargs) + self._has_run_n_evals = True + else: + effective_exit = self._exit_criteria + + if sim_max is not None or getattr(self._exit_criteria, "sim_max", None) is not None: + self._libE_specs.final_gen_send = True + self._libE_specs.reuse_output_dir = True + self.H, self.persis_info, self.flag = libE( self.sim_specs, self.gen_specs, - self.exit_criteria, + effective_exit, persis_info=self.persis_info, alloc_specs=self.alloc_specs, libE_specs=self._libE_specs, H0=self.H0, ) + # Chain history for next call + self.H0 = self.H + return self.H, self.persis_info, self.flag @property diff --git a/libensemble/gen_classes/preloaded.py b/libensemble/gen_classes/preloaded.py index d032e205e..02e5a9b38 100644 --- a/libensemble/gen_classes/preloaded.py +++ b/libensemble/gen_classes/preloaded.py @@ -9,7 +9,7 @@ import numpy as np from libensemble import Ensemble from libensemble.gen_classes.preloaded import PreloadedSampleGenerator - from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + from libensemble.specs import GenSpecs, SimSpecs H0 = np.zeros(500, dtype=[("x", float, 8), ("sim_id", int)]) H0["x"] = my_existing_points @@ -18,8 +18,7 @@ sampling = Ensemble(parse_args=True) sampling.gen_specs = GenSpecs(generator=PreloadedSampleGenerator(H0)) sampling.sim_specs = SimSpecs(sim_f=my_sim, inputs=["x"], out=[("f", float)]) - sampling.exit_criteria = ExitCriteria(sim_max=len(H0)) - sampling.run() + sampling.run(sim_max=len(H0)) This replaces the legacy ``give_pregenerated_work`` allocator pattern, which required a custom ``AllocSpecs`` and bypassed the generator entirely. With @@ -84,7 +83,7 @@ class PreloadedSampleGenerator(Generator): from libensemble import Ensemble from libensemble.gen_classes.preloaded import PreloadedSampleGenerator from libensemble.sim_funcs.borehole import borehole as sim_f, gen_borehole_input - from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + from libensemble.specs import GenSpecs, SimSpecs n_samp = 1000 pts = np.zeros(n_samp, dtype=[("x", float, 8)]) @@ -93,8 +92,7 @@ class PreloadedSampleGenerator(Generator): sampling = Ensemble(parse_args=True) sampling.gen_specs = GenSpecs(generator=PreloadedSampleGenerator(pts)) sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) - sampling.exit_criteria = ExitCriteria(sim_max=n_samp) - sampling.run() + sampling.run(sim_max=n_samp) """ def __init__( diff --git a/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py b/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py index 90dab7507..904224414 100644 --- a/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py +++ b/libensemble/tests/functionality_tests/test_1d_sampling_no_comms_given.py @@ -22,7 +22,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs from libensemble.tools import check_npy_file_exists # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). @@ -46,17 +46,14 @@ }, ) - exit_criteria = ExitCriteria(gen_max=501) - sampling = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) - H, persis_info, flag = sampling.run() + H, persis_info, flag = sampling.run(gen_max=501) if sampling.is_manager: assert len(H) >= 501 diff --git a/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py b/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py index 463fbbb0e..b50df3a23 100644 --- a/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py +++ b/libensemble/tests/functionality_tests/test_asktell_sampling_external_gen.py @@ -22,7 +22,7 @@ # from libensemble.gen_classes.external.sampling import UniformSampleArray from libensemble.gen_classes.external.sampling import UniformSample -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs # Import libEnsemble items for this test @@ -75,17 +75,14 @@ def sim_f_scalar(In): vocs=vocs, ) - exit_criteria = ExitCriteria(gen_max=201) - ensemble = Ensemble( parse_args=True, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, libE_specs=libE_specs, ) - ensemble.run() + ensemble.run(gen_max=201) if ensemble.is_manager: print(ensemble.H[["sim_id", "x0", "x1", "f"]][:10]) diff --git a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py index 3e37bc86d..a3d7da360 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -21,7 +21,7 @@ from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first from libensemble.gen_funcs.sampling import latin_hypercube_sample as gen_f from libensemble.sim_funcs.six_hump_camel import six_hump_camel as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, SimSpecs def create_H0(gen_specs, H0_size): @@ -55,14 +55,14 @@ def create_H0(gen_specs, H0_size): }, } sampling.gen_specs = GenSpecs(**gen_specs) - sampling.exit_criteria = ExitCriteria(sim_max=100) - sampling.H0 = create_H0(gen_specs, 50) + H0 = create_H0(gen_specs, 50) + sampling.H0 = H0 sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) - sampling.run() + sampling.run(sim_max=100) if sampling.is_manager: - assert len(sampling.H) == 2 * len(sampling.H0) - assert np.array_equal(sampling.H0["x"][:50], sampling.H["x"][:50]) + assert len(sampling.H) == 2 * len(H0) + assert np.array_equal(H0["x"][:50], sampling.H["x"][:50]) assert np.all(sampling.H["sim_ended"]) assert np.all(sampling.H["gen_worker"] == 0) print("\nlibEnsemble correctly appended to the initial sample via an additional gen.") diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py index d6b368b93..d33316b2d 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py @@ -7,7 +7,7 @@ from libensemble import Ensemble from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -52,8 +52,5 @@ # Starts one persistent generator. Simulated values are returned in batch. - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=8) diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py index 2a6cda15b..77aca1d5d 100644 --- a/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py +++ b/libensemble/tests/functionality_tests/test_executor_forces_tutorial_2.py @@ -7,7 +7,7 @@ from libensemble import Ensemble, logger from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs logger.set_level("DEBUG") @@ -54,10 +54,7 @@ # Starts one persistent generator. Simulated values are returned in batch. - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=8) ensemble.save_output(__file__) diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py index 3c6fa0b32..4abffc5a2 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py @@ -4,7 +4,7 @@ from sine_sim import sim_find_sine from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Python-quirk required on macOS and windows libE_specs = LibeSpecs(nworkers=4, comms="local") @@ -25,10 +25,8 @@ out=[("y", float)], # sim_f output. "y" = sine("x") ) # sim_specs_end_tag - exit_criteria = ExitCriteria(sim_max=80) # Stop libEnsemble after 80 simulations - - ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs) - ensemble.run() # start the ensemble. Blocks until completion. + ensemble = Ensemble(sim_specs, gen_specs, libE_specs=libE_specs) + ensemble.run(sim_max=80) # start the ensemble. Blocks until completion. history = ensemble.H # start visualizing our results diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py index 286a0ecdc..8c794298c 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial_2.py @@ -4,7 +4,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": libE_specs = LibeSpecs(nworkers=4, comms="local") @@ -27,10 +27,8 @@ alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) - exit_criteria = ExitCriteria(gen_max=160) - - ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, libE_specs, alloc_specs) - ensemble.run() + ensemble = Ensemble(sim_specs, gen_specs, libE_specs=libE_specs, alloc_specs=alloc_specs) + ensemble.run(gen_max=160) if ensemble.flag != 0: print("Oh no! An error occurred!") diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py index 988fe0e78..2b54d37c5 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial_3.py @@ -4,7 +4,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, SimSpecs if __name__ == "__main__": # Python-quirk required on macOS and windows # libE_specs = LibeSpecs(nworkers=4, comms="local") @@ -25,14 +25,12 @@ out=[("y", float)], # sim_f output. "y" = sine("x") ) # sim_specs_end_tag - exit_criteria = ExitCriteria(sim_max=80) # Stop libEnsemble after 80 simulations - alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) # replace libE_specs with parse_args=True. Detects MPI runtime - ensemble = Ensemble(sim_specs, gen_specs, exit_criteria, alloc_specs=alloc_specs, parse_args=True) + ensemble = Ensemble(sim_specs, gen_specs, alloc_specs=alloc_specs, parse_args=True) - ensemble.run() # start the ensemble. Blocks until completion. + ensemble.run(sim_max=80) # start the ensemble. Blocks until completion. if ensemble.is_manager: # only True on rank 0 history = ensemble.H # start visualizing our results diff --git a/libensemble/tests/functionality_tests/test_mpi_warning.py b/libensemble/tests/functionality_tests/test_mpi_warning.py index f58620b90..7edaa1eae 100644 --- a/libensemble/tests/functionality_tests/test_mpi_warning.py +++ b/libensemble/tests/functionality_tests/test_mpi_warning.py @@ -22,7 +22,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, SimSpecs # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -44,13 +44,11 @@ ) sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) - sampling.exit_criteria = ExitCriteria(sim_max=100) - if sampling.is_manager: if os.path.exists(log_file): os.remove(log_file) - sampling.run() + sampling.run(sim_max=100) if sampling.is_manager: print("len:", len(sampling.H)) time.sleep(0.2) diff --git a/libensemble/tests/regression_tests/test_1d_sampling.py b/libensemble/tests/regression_tests/test_1d_sampling.py index 456b3ca60..3bbccdead 100644 --- a/libensemble/tests/regression_tests/test_1d_sampling.py +++ b/libensemble/tests/regression_tests/test_1d_sampling.py @@ -20,7 +20,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": sampling = Ensemble(parse_args=True) @@ -37,9 +37,7 @@ }, ) - sampling.exit_criteria = ExitCriteria(sim_max=500) - - sampling.run() + sampling.run(sim_max=500) if sampling.is_manager: assert len(sampling.H) >= 500 print("\nlibEnsemble with random sampling has generated enough points") diff --git a/libensemble/tests/regression_tests/test_2d_sampling.py b/libensemble/tests/regression_tests/test_2d_sampling.py index c26a66201..d3c783d75 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling.py +++ b/libensemble/tests/regression_tests/test_2d_sampling.py @@ -21,7 +21,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.simple_sim import norm_eval as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -40,9 +40,7 @@ sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) - sampling.exit_criteria = ExitCriteria(sim_max=200) - - sampling.run() + sampling.run(sim_max=200) if sampling.is_manager: assert len(sampling.H) >= 200 x = sampling.H["x"] diff --git a/libensemble/tests/regression_tests/test_2d_sampling_vocs.py b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py index f535e1709..563a95162 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling_vocs.py +++ b/libensemble/tests/regression_tests/test_2d_sampling_vocs.py @@ -17,7 +17,7 @@ from libensemble import Ensemble from libensemble.gen_classes.sampling import LatinHypercubeSample -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def sim_f(In, persis_info, sim_specs, _): @@ -45,14 +45,12 @@ def sim_f(In, persis_info, sim_specs, _): batch_size=100, ) - sampling.exit_criteria = ExitCriteria(sim_max=200) - - sampling.run() + sampling.run(sim_max=200) if sampling.is_manager: assert len(sampling.H) >= 200 x0 = sampling.H["x0"] x1 = sampling.H["x1"] f = sampling.H["f"] - assert np.all(np.isclose(f, np.sqrt(x0 ** 2 + x1 ** 2))) + assert np.all(np.isclose(f, np.sqrt(x0**2 + x1**2))) print("\nlibEnsemble has calculated the 2D vector norm of all points") sampling.save_output(__file__) diff --git a/libensemble/tests/regression_tests/test_GPU_variable_resources.py b/libensemble/tests/regression_tests/test_GPU_variable_resources.py index c8455b459..357184a2a 100644 --- a/libensemble/tests/regression_tests/test_GPU_variable_resources.py +++ b/libensemble/tests/regression_tests/test_GPU_variable_resources.py @@ -35,7 +35,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs # logger.set_level("DEBUG") # For testing the test @@ -78,18 +78,15 @@ # Run with random num_procs/num_gpus for each simulation gpu_test.persis_info = {} - gpu_test.exit_criteria = ExitCriteria(sim_max=10) - - gpu_test.run() + gpu_test.run(sim_max=10) if gpu_test.is_manager: assert gpu_test.flag == 0 - # Run with num_gpus based on x[0] for each simulation + # Run with num_gpus based on x[0] for each simulation (independent run) gpu_test.gen_specs.gen_f = gen_f2 gpu_test.gen_specs.user["max_gpus"] = gpu_test.nworkers - 1 - gpu_test.persis_info = {} - gpu_test.exit_criteria = ExitCriteria(sim_max=20) - gpu_test.run() + gpu_test.reset() + gpu_test.run(sim_max=20) if gpu_test.is_manager: assert gpu_test.flag == 0 diff --git a/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py b/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py index 5564e7f96..38fb327f0 100644 --- a/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py +++ b/libensemble/tests/regression_tests/test_GPU_variable_resources_multi_task.py @@ -45,7 +45,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs import six_hump_camel from libensemble.sim_funcs.var_resources import gpu_variable_resources_from_gen as sim_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs # logger.set_level("DEBUG") # For testing the test @@ -87,10 +87,8 @@ }, ) - gpu_test.exit_criteria = ExitCriteria(sim_max=10, wallclock_max=300) - if gpu_test.ready(): - gpu_test.run() + gpu_test.run(sim_max=10, wallclock_max=300) if gpu_test.is_manager: assert gpu_test.flag == 0 diff --git a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py index 2a68d6e72..1427edcf0 100644 --- a/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py @@ -30,7 +30,7 @@ from libensemble import Ensemble from libensemble.gen_classes import APOSMM -from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import GenSpecs, SimSpecs from libensemble.tests.regression_tests.support import six_hump_camel_minima as minima @@ -90,10 +90,8 @@ def six_hump_camel_func(x): ) workflow.sim_specs = SimSpecs(simulator=six_hump_camel_func, vocs=vocs) - workflow.exit_criteria = ExitCriteria(sim_max=3000, wallclock_max=600) - # Perform the run - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=3000, wallclock_max=600) if workflow.is_manager: print("[Manager]:", H[np.where(H["local_min"])]["x"]) diff --git a/libensemble/tests/regression_tests/test_evaluate_existing_sample.py b/libensemble/tests/regression_tests/test_evaluate_existing_sample.py index 8f1ee674c..0c3e69911 100644 --- a/libensemble/tests/regression_tests/test_evaluate_existing_sample.py +++ b/libensemble/tests/regression_tests/test_evaluate_existing_sample.py @@ -21,7 +21,7 @@ from libensemble.alloc_funcs.give_pregenerated_work import give_pregenerated_sim_work as alloc_f from libensemble.sim_funcs.borehole import borehole as sim_f from libensemble.sim_funcs.borehole import gen_borehole_input -from libensemble.specs import AllocSpecs, ExitCriteria, SimSpecs +from libensemble.specs import AllocSpecs, SimSpecs # Main block is necessary only when using local comms with spawn start method (default on macOS and Windows). if __name__ == "__main__": @@ -36,8 +36,7 @@ sampling.H0 = H0 sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) - sampling.exit_criteria = ExitCriteria(sim_max=len(H0)) - sampling.run() + sampling.run(sim_max=len(H0)) if sampling.is_manager: assert len(sampling.H) == len(H0) diff --git a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py index 60e43fa57..f61689f4f 100644 --- a/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py +++ b/libensemble/tests/regression_tests/test_evaluate_mixed_sample.py @@ -24,7 +24,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.borehole import borehole as sim_f from libensemble.sim_funcs.borehole import borehole_func, gen_borehole_input -from libensemble.specs import AllocSpecs, ExitCriteria, SimSpecs +from libensemble.specs import AllocSpecs, SimSpecs warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -47,8 +47,7 @@ sampling.H0 = H0 sampling.sim_specs = SimSpecs(sim_f=sim_f, inputs=["x"], out=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) - sampling.exit_criteria = ExitCriteria(sim_max=len(H0)) - sampling.run() + sampling.run(sim_max=len(H0)) if sampling.is_manager: assert len(sampling.H) == len(H0) diff --git a/libensemble/tests/regression_tests/test_inverse_bayes_example.py b/libensemble/tests/regression_tests/test_inverse_bayes_example.py index 72fa6eed0..23a1df9ad 100644 --- a/libensemble/tests/regression_tests/test_inverse_bayes_example.py +++ b/libensemble/tests/regression_tests/test_inverse_bayes_example.py @@ -24,7 +24,7 @@ from libensemble.alloc_funcs.inverse_bayes_allocf import only_persistent_gens_for_inverse_bayes as alloc_f from libensemble.gen_funcs.persistent_inverse_bayes import persistent_updater_after_likelihood as gen_f from libensemble.sim_funcs.inverse_bayes import likelihood_calculator as sim_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, SimSpecs if __name__ == "__main__": # Parse args for test code @@ -59,10 +59,9 @@ bayes_test.persis_info = {} gen_user = bayes_test.gen_specs.user val = gen_user["subbatch_size"] * gen_user["num_subbatches"] * gen_user["num_batches"] - bayes_test.exit_criteria = ExitCriteria(sim_max=val, wallclock_max=300) # Perform the run - H, _, flag = bayes_test.run() + H, _, flag = bayes_test.run(sim_max=val, wallclock_max=300) if bayes_test.is_manager: assert flag == 0 diff --git a/libensemble/tests/regression_tests/test_optimas_ax_mf.py b/libensemble/tests/regression_tests/test_optimas_ax_mf.py index fb5b75c32..62ce184db 100644 --- a/libensemble/tests/regression_tests/test_optimas_ax_mf.py +++ b/libensemble/tests/regression_tests/test_optimas_ax_mf.py @@ -23,7 +23,7 @@ from optimas.generators import AxMultiFidelityGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def eval_func_mf(input_params): @@ -61,16 +61,13 @@ def eval_func_mf(input_params): vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=6) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=6) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/regression_tests/test_optimas_ax_multitask.py b/libensemble/tests/regression_tests/test_optimas_ax_multitask.py index 08d97ed6b..cd5ca0615 100644 --- a/libensemble/tests/regression_tests/test_optimas_ax_multitask.py +++ b/libensemble/tests/regression_tests/test_optimas_ax_multitask.py @@ -31,7 +31,7 @@ from optimas.generators import AxMultitaskGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def eval_func_multitask(input_params): @@ -72,8 +72,6 @@ def eval_func_multitask(input_params): vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=15) - H0 = None # or np.load("multitask_first_pass.npy") for run_num in range(2): print(f"\nRun number: {run_num}") @@ -91,11 +89,10 @@ def eval_func_multitask(input_params): libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, H0=H0, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=15) if run_num == 0: H0 = H diff --git a/libensemble/tests/regression_tests/test_optimas_ax_sf.py b/libensemble/tests/regression_tests/test_optimas_ax_sf.py index b9fbbb34b..b91a18beb 100644 --- a/libensemble/tests/regression_tests/test_optimas_ax_sf.py +++ b/libensemble/tests/regression_tests/test_optimas_ax_sf.py @@ -23,7 +23,7 @@ from optimas.generators import AxSingleFidelityGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def eval_func_sf(input_params): @@ -63,16 +63,13 @@ def eval_func_sf(input_params): vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=10) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=10) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/regression_tests/test_optimas_grid_sample.py b/libensemble/tests/regression_tests/test_optimas_grid_sample.py index 5ec670aa9..78d9ec2ed 100644 --- a/libensemble/tests/regression_tests/test_optimas_grid_sample.py +++ b/libensemble/tests/regression_tests/test_optimas_grid_sample.py @@ -24,7 +24,7 @@ from optimas.generators import GridSamplingGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def eval_func(input_params: dict): @@ -72,16 +72,13 @@ def eval_func(input_params: dict): vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=n_evals) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=n_evals) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py b/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py index c0f2cff17..b0c7f4b6a 100644 --- a/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py +++ b/libensemble/tests/regression_tests/test_persistent_fd_param_finder.py @@ -28,7 +28,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.noisy_vector_mapping import func_wrapper as sim_f from libensemble.sim_funcs.noisy_vector_mapping import noisy_function -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, SimSpecs if __name__ == "__main__": x0 = np.array([1.23, 4.56]) # point about which we are calculating finite difference parameters @@ -58,16 +58,16 @@ }, ), alloc_specs=AllocSpecs(alloc_f=alloc_f), - exit_criteria=ExitCriteria(gen_max=1000), ) fd_test.persis_info = {} shutil.copy("./scripts_used_by_reg_tests/ECnoise.m", "./") - H, persis_info, _ = fd_test.run() + gen_max = 1000 + H, persis_info, _ = fd_test.run(gen_max=gen_max) if fd_test.is_manager: - assert len(H) < fd_test.exit_criteria.gen_max, "Problem didn't stop early, which should have been the case." + assert len(H) < gen_max, "Problem didn't stop early, which should have been the case." assert np.all(persis_info[0]["Fnoise"] > 0), "gen_f didn't find noise for all F_i components." fd_test.save_output(__file__) diff --git a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py index 3820bfdec..b2f3b6fef 100644 --- a/libensemble/tests/regression_tests/test_persistent_surmise_calib.py +++ b/libensemble/tests/regression_tests/test_persistent_surmise_calib.py @@ -40,7 +40,7 @@ # Import libEnsemble items for this test from libensemble.sim_funcs.surmise_test_function import borehole as sim_f from libensemble.sim_funcs.surmise_test_function import tstd2theta -from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs +from libensemble.specs import GenSpecs, SimSpecs from libensemble.tools import parse_args @@ -98,11 +98,10 @@ def run_surmise_calib(): async_return=True, active_recv_gen=True, ), - exit_criteria=ExitCriteria(sim_max=max_evals), ) # Perform the run - H, _, _ = test.run() + H, _, _ = test.run(sim_max=max_evals) if test.is_manager: print("Cancelled sims", H["sim_id"][H["cancel_requested"]]) diff --git a/libensemble/tests/regression_tests/test_proxystore_integration.py b/libensemble/tests/regression_tests/test_proxystore_integration.py index 22e447286..50b29730e 100644 --- a/libensemble/tests/regression_tests/test_proxystore_integration.py +++ b/libensemble/tests/regression_tests/test_proxystore_integration.py @@ -24,7 +24,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.give_pregenerated_work import give_pregenerated_sim_work as alloc_f from libensemble.sim_funcs.borehole import gen_borehole_input -from libensemble.specs import AllocSpecs, ExitCriteria, SimSpecs +from libensemble.specs import AllocSpecs, SimSpecs def insert_proxy(H0): @@ -79,8 +79,7 @@ def one_d_example(x, persis_info, sim_specs, info): sampling.H0 = H0 sampling.sim_specs = SimSpecs(sim_f=one_d_example, inputs=["x", "proxy"], outputs=[("f", float)]) sampling.alloc_specs = AllocSpecs(alloc_f=alloc_f) - sampling.exit_criteria = ExitCriteria(sim_max=len(H0)) - sampling.run() + sampling.run(sim_max=len(H0)) if sampling.is_manager: assert len(sampling.H) == len(H0) diff --git a/libensemble/tests/regression_tests/test_xopt_EI.py b/libensemble/tests/regression_tests/test_xopt_EI.py index 7fb158b58..f45d58d26 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI.py +++ b/libensemble/tests/regression_tests/test_xopt_EI.py @@ -23,7 +23,7 @@ from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs # Adapted from Xopt/xopt/resources/testing.py @@ -84,16 +84,13 @@ def xtest_sim(H, persis_info, sim_specs, _): vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=20) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=20) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py index d89de9449..8394308c8 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample.py @@ -25,7 +25,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs def xtest_sim(H, persis_info, sim_specs, _): @@ -69,17 +69,15 @@ def xtest_sim(H, persis_info, sim_specs, _): ) alloc_specs = AllocSpecs(alloc_f=alloc_f) - exit_criteria = ExitCriteria(sim_max=20) workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, alloc_specs=alloc_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=20) if workflow.is_manager: print(f"Completed {len(H)} simulations") diff --git a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py index c7db6d363..08e02b6ec 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py +++ b/libensemble/tests/regression_tests/test_xopt_EI_initial_sample_instance.py @@ -27,7 +27,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.gen_classes.sampling import LatinHypercubeSample -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs def xtest_sim(H, persis_info, sim_specs, _): @@ -74,17 +74,15 @@ def xtest_sim(H, persis_info, sim_specs, _): ) alloc_specs = AllocSpecs(alloc_f=alloc_f) - exit_criteria = ExitCriteria(sim_max=20) workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, alloc_specs=alloc_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=20) if workflow.is_manager: print(f"Completed {len(H)} simulations") diff --git a/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py b/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py index 13939169f..218ce5e90 100644 --- a/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py +++ b/libensemble/tests/regression_tests/test_xopt_EI_xopt_sim.py @@ -23,7 +23,7 @@ from xopt.generators.bayesian.expected_improvement import ExpectedImprovementGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs # From Xopt/xopt/resources/testing.py @@ -78,16 +78,13 @@ def xtest_callable(input_dict: dict, a=0) -> dict: vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=20) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=20) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/regression_tests/test_xopt_nelder_mead.py b/libensemble/tests/regression_tests/test_xopt_nelder_mead.py index 30d095207..288e45824 100644 --- a/libensemble/tests/regression_tests/test_xopt_nelder_mead.py +++ b/libensemble/tests/regression_tests/test_xopt_nelder_mead.py @@ -21,7 +21,7 @@ from xopt.generators.sequential.neldermead import NelderMeadGenerator from libensemble import Ensemble -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs def rosenbrock_callable(input_dict: dict) -> dict: @@ -66,16 +66,13 @@ def rosenbrock_callable(input_dict: dict) -> dict: vocs=vocs, ) - exit_criteria = ExitCriteria(sim_max=30) - workflow = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, - exit_criteria=exit_criteria, ) - H, _, _ = workflow.run() + H, _, _ = workflow.run(sim_max=30) # Perform the run if workflow.is_manager: diff --git a/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py index 7b45ebd57..4b4513ea9 100644 --- a/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_gpu/run_libe_forces.py @@ -26,7 +26,7 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -75,15 +75,13 @@ }, ) - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + sim_max = 8 + ensemble.run(sim_max=sim_max) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. - if ensemble.exit_criteria.sim_max == 8: + if sim_max == 8: chksum = np.sum(ensemble.H["energy"]) assert np.isclose(chksum, 96288744.35136001), f"energy check sum is {chksum}" print("Checksum passed") diff --git a/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py index 09a43e175..1f0ad675e 100644 --- a/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_gpu_var_resources/run_libe_forces.py @@ -29,7 +29,7 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_with_var_gpus as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -83,15 +83,13 @@ }, ) - # Instruct libEnsemble to exit after this many simulations. - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + sim_max = 8 + ensemble.run(sim_max=sim_max) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. - if ensemble.exit_criteria.sim_max == 8: + if sim_max == 8: chksum = np.sum(ensemble.H["energy"]) assert np.isclose(chksum, 96288744.35136001), f"energy check sum is {chksum}" print("Checksum passed") diff --git a/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py index e5faeb9b4..b71c8ce50 100644 --- a/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_multi_app/run_libe_forces.py @@ -33,7 +33,7 @@ from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling_var_resources import uniform_sample_diff_simulations as gen_f -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor instance @@ -95,11 +95,9 @@ }, ) - # Instruct libEnsemble to exit after this many simulations. - ensemble.exit_criteria = ExitCriteria(sim_max=nsim_workers * 2) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + sim_max = nsim_workers * 2 + ensemble.run(sim_max=sim_max) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. @@ -107,7 +105,7 @@ print(f"Final energy checksum: {chksum}") exp_chksums = {16: -21935405.696289998, 32: -26563930.6356} - exp_chksum = exp_chksums.get(ensemble.exit_criteria.sim_max) + exp_chksum = exp_chksums.get(sim_max) if exp_chksum is not None: assert np.isclose(chksum, exp_chksum), f"energy check sum is {chksum}" diff --git a/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py index a337a09e4..8c99fe2dd 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py @@ -9,7 +9,7 @@ from libensemble import Ensemble from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -54,11 +54,8 @@ # Starts one persistent generator. Simulated values are returned in batch. - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=8) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py index 9c066ec93..189443b9b 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_simple_with_input_file/run_libe_forces.py @@ -9,7 +9,7 @@ from libensemble import Ensemble from libensemble.executors import MPIExecutor from libensemble.gen_funcs.persistent_sampling import persistent_uniform as gen_f -from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs if __name__ == "__main__": # Initialize MPI Executor @@ -58,11 +58,8 @@ # Starts one persistent generator. Simulated values are returned in batch. - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=8) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. diff --git a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py index 2d23c83a8..c3e5024f7 100644 --- a/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py +++ b/libensemble/tests/scaling_tests/forces/forces_simple_xopt/run_libe_forces.py @@ -10,7 +10,7 @@ from libensemble import Ensemble from libensemble.alloc_funcs.start_only_persistent import only_persistent_gens as alloc_f from libensemble.executors import MPIExecutor -from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs +from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs # from forces_simf import run_forces_dict # gest-api/xopt style simulator. @@ -64,11 +64,8 @@ }, ) - # Instruct libEnsemble to exit after this many simulations - ensemble.exit_criteria = ExitCriteria(sim_max=8) - - # Run ensemble - ensemble.run() + # Run ensemble; exit after this many simulations + ensemble.run(sim_max=8) if ensemble.is_manager: # Note, this will change if changing sim_max, nworkers, lb, ub, etc. diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index 0e5de3223..9de46c2f7 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -42,7 +42,7 @@ def test_full_workflow(): from libensemble.ensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import AllocSpecs, ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs LS = LibeSpecs(comms="local", nworkers=4) @@ -60,11 +60,10 @@ def test_full_workflow(): "ub": np.array([3]), }, ), - exit_criteria=ExitCriteria(gen_max=101), alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), ) - ens.run() + ens.run(gen_max=101) if ens.is_manager: assert len(ens.H) >= 101 @@ -72,7 +71,7 @@ def test_full_workflow(): ens.libE_specs.dry_run = True flag = 1 try: - ens.run() + ens.run(gen_max=101) except SystemExit: flag = 0 assert not flag, "Ensemble didn't exit after specifying dry_run" @@ -85,7 +84,7 @@ def test_flakey_workflow(): from libensemble.ensemble import Ensemble from libensemble.gen_funcs.sampling import latin_hypercube_sample from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs LS = LibeSpecs(comms="local", nworkers=4) @@ -102,10 +101,9 @@ def test_flakey_workflow(): "ub": np.array([3]), }, ), - exit_criteria=ExitCriteria(gen_max=101), ) ens.sim_specs.inputs = (["x"],) # note trailing comma - ens.run() + ens.run(gen_max=101) except ValidationError: flag = 0 @@ -185,13 +183,13 @@ def test_local_comms_without_nworkers(): def test_ready_missing_sim_callable(): """ready() should flag a missing sim callable.""" from libensemble.ensemble import Ensemble - from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + from libensemble.specs import LibeSpecs, SimSpecs e = Ensemble( libE_specs=LibeSpecs(comms="local", nworkers=4), sim_specs=SimSpecs(), # no sim_f or simulator - exit_criteria=ExitCriteria(sim_max=10), ) + e._exit_criteria.sim_max = 10 # set directly to avoid deprecation warning ok, issues = e.ready() assert not ok, "Should not be ready without a sim callable" assert any("sim_f" in msg for msg in issues), f"Expected sim_f mention in issues: {issues}" @@ -201,12 +199,12 @@ def test_ready_missing_exit_criteria(): """ready() should flag an exit_criteria with no stop condition.""" from libensemble.ensemble import Ensemble from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + from libensemble.specs import LibeSpecs, SimSpecs e = Ensemble( libE_specs=LibeSpecs(comms="local", nworkers=4), sim_specs=SimSpecs(sim_f=norm_eval), - exit_criteria=ExitCriteria(), # nothing set + # no exit criteria set — _exit_criteria defaults to ExitCriteria() with nothing set ) ok, issues = e.ready() assert not ok, "Should not be ready with no exit condition" @@ -217,15 +215,15 @@ def test_ready_missing_nworkers_local(): """ready() should flag local comms without nworkers.""" from libensemble.ensemble import Ensemble from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, LibeSpecs, SimSpecs + from libensemble.specs import LibeSpecs, SimSpecs # Bypass the constructor ValueError by using mpi comms first, # then patch to local after construction. e = Ensemble( libE_specs=LibeSpecs(comms="mpi"), sim_specs=SimSpecs(sim_f=norm_eval), - exit_criteria=ExitCriteria(sim_max=10), ) + e._exit_criteria.sim_max = 10 # set directly to avoid deprecation warning # Manually force comms=local and nworkers=0 on the internal specs object e._libE_specs.comms = "local" e._nworkers = 0 @@ -240,14 +238,14 @@ def test_ready_field_mismatch(): """ready() should flag when sim_specs.inputs requests fields not in gen_specs.outputs.""" from libensemble.ensemble import Ensemble from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs e = Ensemble( libE_specs=LibeSpecs(comms="local", nworkers=4), sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x", "z"]), gen_specs=GenSpecs(outputs=[("x", float, (1,))]), # missing "z" - exit_criteria=ExitCriteria(sim_max=10), ) + e._exit_criteria.sim_max = 10 # set directly to avoid deprecation warning ok, issues = e.ready() assert not ok, "Should not be ready with mismatched gen/sim fields" assert any("z" in msg for msg in issues), f"Expected missing field 'z' in issues: {issues}" @@ -257,19 +255,188 @@ def test_ready_happy_path(): """ready() should return (True, []) for a fully configured ensemble.""" from libensemble.ensemble import Ensemble from libensemble.sim_funcs.simple_sim import norm_eval - from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs + from libensemble.specs import GenSpecs, LibeSpecs, SimSpecs e = Ensemble( libE_specs=LibeSpecs(comms="local", nworkers=4), sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), gen_specs=GenSpecs(outputs=[("x", float, (1,))]), - exit_criteria=ExitCriteria(sim_max=10), ) + e._exit_criteria.sim_max = 10 # set directly to avoid deprecation warning ok, issues = e.ready() assert ok, f"Should be ready but got issues: {issues}" assert issues == [], f"Issues should be empty but got: {issues}" +# --- run() kwargs / substep tests --- + + +def test_run_sim_max_kwarg(): + """run(sim_max=10) should evaluate exactly 10 simulations.""" + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first + from libensemble.ensemble import Ensemble + from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs + + ens = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs( + gen_f=latin_hypercube_sample, + outputs=[("x", float, (1,))], + persis_in=["f"], + batch_size=5, + user={"lb": np.array([-3]), "ub": np.array([3])}, + ), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + ens.run(sim_max=10) + if ens.is_manager: + sim_count = int(np.sum(ens.H["sim_ended"])) + assert sim_count == 10, f"Expected 10 sims but got {sim_count}" + + +def test_run_chaining(): + """Two run(sim_max=N) calls should chain H0, doubling total.""" + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first + from libensemble.ensemble import Ensemble + from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs + + ens = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs( + gen_f=latin_hypercube_sample, + outputs=[("x", float, (1,))], + persis_in=["f"], + batch_size=5, + user={"lb": np.array([-3]), "ub": np.array([3])}, + ), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + ens.run(sim_max=10) + h1_ended = int(np.sum(ens.H["sim_ended"])) if ens.is_manager else 0 + ens.run(sim_max=10) + if ens.is_manager: + total_ended = int(np.sum(ens.H["sim_ended"])) + assert total_ended == h1_ended + 10, f"Expected {h1_ended + 10} sims ended but got {total_ended}" + assert ens.H0 is ens.H, "H0 should reference the latest H" + + +def test_run_sim_max_merge(): + """run() kwargs should merge with existing exit_criteria, not replace.""" + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first + from libensemble.ensemble import Ensemble + from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs + + # Must have full sim/gen specs so run() actually works + ens = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs( + gen_f=latin_hypercube_sample, + outputs=[("x", float, (1,))], + persis_in=["f"], + batch_size=5, + user={"lb": np.array([-3]), "ub": np.array([3])}, + ), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + ens._exit_criteria.sim_max = 100 # set directly to avoid deprecation warning + ens.run(sim_max=10) + # stored exit_criteria should still have sim_max=100 + assert ens.exit_criteria.sim_max == 100, f"Expected sim_max=100 but got {ens.exit_criteria.sim_max}" + + +def test_exit_criteria_deprecation_init(): + """Passing ExitCriteria to Ensemble() should emit a deprecation warning.""" + import warnings + + from libensemble._deprecation import LibEnsembleDeprecationWarning + from libensemble.ensemble import Ensemble + from libensemble.specs import ExitCriteria + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + Ensemble(exit_criteria=ExitCriteria(sim_max=10)) + deprecations = [x for x in w if issubclass(x.category, LibEnsembleDeprecationWarning)] + assert len(deprecations) >= 1, "Expected at least one LibEnsembleDeprecationWarning" + + +def test_exit_criteria_deprecation_setter(): + """Setting ensemble.exit_criteria = ExitCriteria(...) should emit a deprecation warning.""" + import warnings + + from libensemble._deprecation import LibEnsembleDeprecationWarning + from libensemble.ensemble import Ensemble + from libensemble.specs import ExitCriteria, LibeSpecs + + ens = Ensemble(libE_specs=LibeSpecs(comms="local", nworkers=4)) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + ens.exit_criteria = ExitCriteria(sim_max=10) + deprecations = [x for x in w if issubclass(x.category, LibEnsembleDeprecationWarning)] + assert len(deprecations) >= 1, "Expected at least one LibEnsembleDeprecationWarning" + + +def test_run_auto_settings(): + """run(sim_max=...) should auto-set final_gen_send and reuse_output_dir.""" + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first + from libensemble.ensemble import Ensemble + from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs + + ens = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs( + gen_f=latin_hypercube_sample, + outputs=[("x", float, (1,))], + persis_in=["f"], + batch_size=5, + user={"lb": np.array([-3]), "ub": np.array([3])}, + ), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + ens.run(sim_max=10) + assert ens.libE_specs.final_gen_send is True + assert ens.libE_specs.reuse_output_dir is True + + +def test_h0_chaining_plain_run(): + """H0 should be updated to H after a plain run() call.""" + from libensemble.alloc_funcs.give_sim_work_first import give_sim_work_first + from libensemble.ensemble import Ensemble + from libensemble.gen_funcs.sampling import latin_hypercube_sample + from libensemble.sim_funcs.simple_sim import norm_eval + from libensemble.specs import AllocSpecs, GenSpecs, LibeSpecs, SimSpecs + + ens = Ensemble( + libE_specs=LibeSpecs(comms="local", nworkers=4), + sim_specs=SimSpecs(sim_f=norm_eval, inputs=["x"], outputs=[("f", float)]), + gen_specs=GenSpecs( + gen_f=latin_hypercube_sample, + outputs=[("x", float, (1,))], + persis_in=["f"], + batch_size=5, + user={"lb": np.array([-3]), "ub": np.array([3])}, + ), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + assert ens.H0 is None, "H0 should be None before first run" + ens.run(sim_max=5) + if ens.is_manager: + assert ens.H0 is not None, "H0 should be set after run" + sim_count = int(np.sum(ens.H0["sim_ended"])) + assert sim_count == 5, f"Expected H0 sim_ended count 5 but got {sim_count}" + + if __name__ == "__main__": test_ensemble_init() test_ensemble_parse_args_false() @@ -283,3 +450,10 @@ def test_ready_happy_path(): test_ready_missing_nworkers_local() test_ready_field_mismatch() test_ready_happy_path() + test_run_sim_max_kwarg() + test_run_chaining() + test_run_sim_max_merge() + test_exit_criteria_deprecation_init() + test_exit_criteria_deprecation_setter() + test_run_auto_settings() + test_h0_chaining_plain_run()