From 7e94385f1e2beccef7a69dbf024b44e3f04145d8 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Jun 2026 13:37:37 -0500 Subject: [PATCH 01/12] Migrate ExitCriteria parameters to Ensemble.run(), e.g. ensemble.run(sim_max=30). merge ExitCriteria and .run parameters if both exist. Display associated deprecation warnings --- libensemble/ensemble.py | 101 +++++++++- libensemble/tests/unit_tests/test_ensemble.py | 176 ++++++++++++++++++ 2 files changed, 272 insertions(+), 5 deletions(-) diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index dc2ac806c..201451dc9 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, @@ -159,7 +168,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 +178,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 +193,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 +268,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 +324,44 @@ 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 _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 +372,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 +421,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/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index dd6f9d1fd..d5af4907f 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -359,6 +359,175 @@ def test_gen_specs_vocs_integer_domain_yields_float_array(): assert gs.user["lb"].dtype == float, "lb should be float dtype even for integer-domain variables" assert gs.user["ub"].dtype == float, "ub should be float dtype even for integer-domain variables" +# --- 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, ExitCriteria, 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])}, + ), + exit_criteria=ExitCriteria(sim_max=100), + alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), + ) + 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 5 sims but got {sim_count}" + + if __name__ == "__main__": test_ensemble_init() @@ -379,3 +548,10 @@ def test_gen_specs_vocs_integer_domain_yields_float_array(): test_gen_specs_no_vocs_leaves_user_empty() test_gen_specs_vocs_satisfies_legacy_user_params() test_gen_specs_vocs_integer_domain_yields_float_array() + 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() From fe371b2336ff3ba629c816ee0c6ccfc5a9eec70e Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jun 2026 14:30:24 -0500 Subject: [PATCH 02/12] reset many tests/examples to use new .run syntax. add .reset() method to prevent state between .runs(). --- docs/examples/calling_scripts.rst | 2 +- docs/platforms/aurora.rst | 4 +- docs/tutorials/aposmm_tutorial.rst | 5 +- docs/tutorials/gpcam_tutorial.rst | 11 ++--- docs/tutorials/xopt_bayesian_gen.rst | 9 ++-- libensemble/ensemble.py | 23 +++++++-- libensemble/gen_classes/preloaded.py | 10 ++-- .../test_1d_sampling_no_comms_given.py | 7 +-- .../test_asktell_sampling_external_gen.py | 7 +-- .../test_evaluate_existing_plus_gen.py | 5 +- .../test_executor_forces_tutorial.py | 9 ++-- .../test_executor_forces_tutorial_2.py | 9 ++-- .../test_local_sine_tutorial.py | 8 ++-- .../test_local_sine_tutorial_2.py | 8 ++-- .../test_local_sine_tutorial_3.py | 8 ++-- .../functionality_tests/test_mpi_warning.py | 6 +-- .../regression_tests/test_1d_sampling.py | 6 +-- .../regression_tests/test_2d_sampling.py | 6 +-- .../regression_tests/test_2d_sampling_vocs.py | 6 +-- .../test_GPU_variable_resources.py | 13 ++--- .../test_GPU_variable_resources_multi_task.py | 6 +-- .../regression_tests/test_aposmm_nlopt.py | 6 +-- .../test_evaluate_existing_sample.py | 5 +- .../test_evaluate_mixed_sample.py | 5 +- .../test_inverse_bayes_example.py | 5 +- .../regression_tests/test_optimas_ax_mf.py | 7 +-- .../test_optimas_ax_multitask.py | 7 +-- .../regression_tests/test_optimas_ax_sf.py | 7 +-- .../test_optimas_grid_sample.py | 7 +-- .../test_persistent_fd_param_finder.py | 8 ++-- .../test_persistent_surmise_calib.py | 5 +- .../test_proxystore_integration.py | 5 +- .../tests/regression_tests/test_xopt_EI.py | 7 +-- .../test_xopt_EI_initial_sample.py | 6 +-- .../test_xopt_EI_initial_sample_instance.py | 6 +-- .../regression_tests/test_xopt_EI_xopt_sim.py | 7 +-- .../regression_tests/test_xopt_nelder_mead.py | 7 +-- .../forces/forces_gpu/run_libe_forces.py | 12 ++--- .../run_libe_forces.py | 12 ++--- .../forces_multi_app/run_libe_forces.py | 12 ++--- .../forces/forces_simple/run_libe_forces.py | 9 ++-- .../run_libe_forces.py | 9 ++-- .../forces_simple_xopt/run_libe_forces.py | 9 ++-- libensemble/tests/unit_tests/test_ensemble.py | 47 ++++++++----------- 44 files changed, 148 insertions(+), 230 deletions(-) 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 660e36322..97e8483ea 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 09578ae1d..3402e5a26 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 3def3a97b..9934d7e8f 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 14e3b50b9..bf73ade13 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 201451dc9..8a642ef1b 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -53,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) @@ -75,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: @@ -334,6 +332,23 @@ def exit_criteria(self, value: ExitCriteria | None): 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 diff --git a/libensemble/gen_classes/preloaded.py b/libensemble/gen_classes/preloaded.py index 9b1337b9f..94f75ca5a 100644 --- a/libensemble/gen_classes/preloaded.py +++ b/libensemble/gen_classes/preloaded.py @@ -11,7 +11,7 @@ from libensemble import Ensemble from libensemble.gen_classes.preloaded import PreloadedSampleGenerator - from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + from libensemble.specs import GenSpecs, SimSpecs vocs = VOCS( # The bounds are metadata only; this generator does not sample from the VOCS. @@ -28,8 +28,7 @@ vocs=vocs, ) sampling.sim_specs = SimSpecs(sim_f=my_sim, vocs=vocs) - 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 @@ -96,7 +95,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 vocs = VOCS( @@ -112,8 +111,7 @@ class PreloadedSampleGenerator(Generator): vocs=vocs, ) sampling.sim_specs = SimSpecs(sim_f=sim_f, vocs=vocs) - 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 f6b703b58..9a767a32e 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 32785fd12..0cc226f3c 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -22,7 +22,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(lb, ub, H0_size): @@ -53,10 +53,9 @@ def create_H0(lb, ub, H0_size): batch_size=50, vocs=vocs, ) - sampling.exit_criteria = ExitCriteria(sim_max=100) sampling.H0 = create_H0(lb, ub, 50) 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) diff --git a/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py b/libensemble/tests/functionality_tests/test_executor_forces_tutorial.py index 78393db20..a8f77eedf 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 @@ -51,8 +51,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 a19bac4bb..988d7726d 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") @@ -53,10 +53,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 257297a6c..b64856c4f 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__": @@ -43,13 +43,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 efa3572cb..70cfa1168 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) @@ -36,9 +36,7 @@ vocs=vocs, ) - 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 bf5c63efc..6c760e0af 100644 --- a/libensemble/tests/regression_tests/test_2d_sampling.py +++ b/libensemble/tests/regression_tests/test_2d_sampling.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 # 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 92382680b..cb0e6a2ea 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,9 +45,7 @@ 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"] 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_aposmm_nlopt.py b/libensemble/tests/regression_tests/test_aposmm_nlopt.py index 40ebcc497..3c545a498 100644 --- a/libensemble/tests/regression_tests/test_aposmm_nlopt.py +++ b/libensemble/tests/regression_tests/test_aposmm_nlopt.py @@ -27,7 +27,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 @@ -86,10 +86,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) - # 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 3aac366e7..2cd0bb3ae 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 38f9566fe..13b781407 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 7676dd807..29324a5f6 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 116ffeb13..ba0d40c4f 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, _): @@ -68,17 +68,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 28a66b076..3fd2843ea 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, _): @@ -73,17 +73,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 d5af4907f..55262428c 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -42,13 +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) @@ -66,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 @@ -78,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" @@ -91,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) @@ -108,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 @@ -190,13 +182,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}" @@ -206,12 +198,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" @@ -222,14 +214,16 @@ 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 # Start valid, then bypass assignment validation to exercise ready(). e = Ensemble( libE_specs=LibeSpecs(comms="local", nworkers=1), 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 e._libE_specs.nworkers = 0 @@ -242,14 +236,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}" @@ -259,14 +253,14 @@ 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}" @@ -359,9 +353,6 @@ def test_gen_specs_vocs_integer_domain_yields_float_array(): assert gs.user["lb"].dtype == float, "lb should be float dtype even for integer-domain variables" assert gs.user["ub"].dtype == float, "ub should be float dtype even for integer-domain variables" -# --- 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 @@ -423,7 +414,7 @@ def test_run_sim_max_merge(): 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 # Must have full sim/gen specs so run() actually works ens = Ensemble( @@ -436,9 +427,9 @@ def test_run_sim_max_merge(): batch_size=5, user={"lb": np.array([-3]), "ub": np.array([3])}, ), - exit_criteria=ExitCriteria(sim_max=100), 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}" From c4d64e8853a426055aa17470cb7786a762b607bc Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jun 2026 15:24:05 -0500 Subject: [PATCH 03/12] fix array-equal comparisons --- .../functionality_tests/test_evaluate_existing_plus_gen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 0cc226f3c..e7f343ff2 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -53,13 +53,14 @@ def create_H0(lb, ub, H0_size): batch_size=50, vocs=vocs, ) - sampling.H0 = create_H0(lb, ub, 50) + H0 = create_H0(lb, ub, 50) + sampling.H0 = H0 sampling.alloc_specs = AllocSpecs(alloc_f=give_sim_work_first) 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.") From 695226c1698e5b7d0b817b93a3d1e7928fbe0437 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 14 Jul 2026 14:05:06 -0500 Subject: [PATCH 04/12] fix bug involving sim_max being set -> always set final_gen_send -> always send data packet using persis_in even if its not set --- libensemble/ensemble.py | 13 ++++++++----- libensemble/tests/unit_tests/test_ensemble.py | 6 +++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index 8a642ef1b..1cfea5a7e 100644 --- a/libensemble/ensemble.py +++ b/libensemble/ensemble.py @@ -402,9 +402,9 @@ def run( 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. + From the second call onward, ``libE_specs.final_gen_send`` and + ``libE_specs.reuse_output_dir`` are automatically set to ``True`` + to support persistent generators across substep runs. Returns ------- @@ -450,11 +450,13 @@ def run( } 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: + # Only activate final_gen_send/reuse_output_dir for substep (multi-call) runs, + # i.e. when a prior run() call has already occurred. A single-call run never + # needs to chain history back to a persistent generator at shutdown. + if self._has_run_n_evals: self._libE_specs.final_gen_send = True self._libE_specs.reuse_output_dir = True @@ -470,6 +472,7 @@ def run( # Chain history for next call self.H0 = self.H + self._has_run_n_evals = True return self.H, self.persis_info, self.flag diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index 55262428c..015c53111 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -467,7 +467,7 @@ def test_exit_criteria_deprecation_setter(): def test_run_auto_settings(): - """run(sim_max=...) should auto-set final_gen_send and reuse_output_dir.""" + """final_gen_send and reuse_output_dir should only be set from the second run() call onward.""" 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 @@ -486,6 +486,10 @@ def test_run_auto_settings(): ), alloc_specs=AllocSpecs(alloc_f=give_sim_work_first), ) + # First run: final_gen_send must NOT be set (this is not a substep run) + ens.run(sim_max=10) + assert not ens.libE_specs.final_gen_send, "final_gen_send should not be set on the first run()" + # Second run: now in substep mode, final_gen_send should be activated ens.run(sim_max=10) assert ens.libE_specs.final_gen_send is True assert ens.libE_specs.reuse_output_dir is True From 5f578e5c7b20c0c17769b18f759128c5d5a006e1 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 10:45:27 -0500 Subject: [PATCH 05/12] pixi update --- pixi.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index 3018c51af..1c1b6fb93 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46081fbc38b6d4a589f028650c593cefc60a375be9d97c8696279ac92966880c -size 1092406 +oid sha256:314cb5c834afa3332fd09c34c18186e18c962ca44c56a7b344b8d5f9dda9000c +size 1098563 From 12f168fb4df69c07dbeb50be5794105d4eacce81 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 10:55:37 -0500 Subject: [PATCH 06/12] Fix test ensemble formatting --- libensemble/tests/unit_tests/test_ensemble.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/unit_tests/test_ensemble.py b/libensemble/tests/unit_tests/test_ensemble.py index 015c53111..3f1524aed 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -353,6 +353,7 @@ def test_gen_specs_vocs_integer_domain_yields_float_array(): assert gs.user["lb"].dtype == float, "lb should be float dtype even for integer-domain variables" assert gs.user["ub"].dtype == float, "ub should be float dtype even for integer-domain variables" + 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 @@ -523,7 +524,6 @@ def test_h0_chaining_plain_run(): assert sim_count == 5, f"Expected 5 sims but got {sim_count}" - if __name__ == "__main__": test_ensemble_init() test_ensemble_parse_args_false() From e601201298de01d67d8dfb5aa2bd2d5a18292e3f Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 14:22:38 -0500 Subject: [PATCH 07/12] obtain required minq commit directly from ibcdfo package. Some interprocess bugfixes in aposmm_localopt_support resulting from opus chasing down this bug --- install/install_minq.sh | 20 ++++++++++- .../gen_funcs/aposmm_localopt_support.py | 33 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/install/install_minq.sh b/install/install_minq.sh index 05caabadc..2a320e438 100644 --- a/install/install_minq.sh +++ b/install/install_minq.sh @@ -1,7 +1,25 @@ #!/usr/bin/env bash +set -euo pipefail + +# IBCDFO pins the exact MINQ commit it supports, and refuses to run (via +# sys.exit) if the MINQ clone is at any other commit. Read the required SHA +# from the installed ibcdfo package rather than hardcoding it here, so that +# bumping ibcdfo in pixi.lock can never silently desynchronize the two. +MINQ_COMMIT=$(python -c " +import pathlib, ibcdfo +print((pathlib.Path(ibcdfo.__file__).parent / 'PkgData' / 'REQUIRED_MINQ_COMMIT').read_text().strip()) +") + +if [ -z "$MINQ_COMMIT" ]; then + echo "ERROR: could not determine required MINQ commit from installed ibcdfo" >&2 + exit 1 +fi + +echo "Installing MINQ at ibcdfo-required commit ${MINQ_COMMIT}" + git clone https://github.com/POptUS/MINQ -git -C MINQ checkout 7749b83645ea21e303a94e1200542f7028499bb8 +git -C MINQ checkout "$MINQ_COMMIT" pushd MINQ/py/minq5/ export PYTHONPATH="$PYTHONPATH:$(pwd)" echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV diff --git a/libensemble/gen_funcs/aposmm_localopt_support.py b/libensemble/gen_funcs/aposmm_localopt_support.py index 5f0bd67e6..8c942baa7 100644 --- a/libensemble/gen_funcs/aposmm_localopt_support.py +++ b/libensemble/gen_funcs/aposmm_localopt_support.py @@ -143,7 +143,7 @@ def __init__(self, user_specs, x0, f0, grad0=None): self.process.start() self.is_running = True - self.parent_can_read.wait() + self._wait_for_child() x_new = self.comm_queue.get() if isinstance(x_new, ErrorMsg): raise APOSMMException(x_new.x) @@ -173,7 +173,7 @@ def iterate(self, data): self.comm_queue.put((data["x_on_cube"], data["f"])) self.child_can_read.set() - self.parent_can_read.wait() + self._wait_for_child() x_new = self.comm_queue.get() if isinstance(x_new, ErrorMsg): @@ -185,6 +185,28 @@ def iterate(self, data): return x_new + def _wait_for_child(self, poll_interval=0.2): + """Wait for the optimizer child process to signal that it has produced a result. + + Rather than blocking forever on ``parent_can_read``, poll the event while + also checking that the child is still alive. A child that dies abruptly + (segfault, os._exit, or a SystemExit raised before the handler can run) + would otherwise never set the event, hanging the generator -- and with it + the whole ensemble -- indefinitely. + """ + while not self.parent_can_read.wait(timeout=poll_interval): + if not self.process.is_alive(): + # Child exited. Give it a final chance to have set the event + # concurrently with our liveness check before declaring failure. + if self.parent_can_read.is_set(): + break + raise APOSMMException( + "APOSMM Error: the local optimizer subprocess exited unexpectedly " + f"(exitcode {self.process.exitcode}) without returning a result. " + "Check the output above for errors raised by the optimizer, and verify " + "that the optimizer and any of its dependencies are correctly installed." + ) + def destroy(self): """Recursively kill any optimizer processes still running""" if self.process.is_alive(): @@ -654,7 +676,12 @@ def run_local_tao(user_specs, comm_queue, x0, f0, child_can_read, parent_can_rea def opt_runner(run_local_opt, user_specs, comm_queue, x0, f0, child_can_read, parent_can_read): try: run_local_opt(user_specs, comm_queue, x0, f0, child_can_read, parent_can_read) - except Exception: + except BaseException: + # Must catch BaseException, not Exception: some optimizers (e.g. IBCDFO + # when its MINQ dependency is at an unsupported commit) abort via + # sys.exit(), which raises SystemExit. SystemExit inherits from + # BaseException, so an `except Exception` here would let the child die + # silently without ever setting parent_can_read, deadlocking the parent. comm_queue.put(ErrorMsg(traceback.format_exc())) parent_can_read.set() From 055706832b143604b6882194d941f9e66d17c492 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 17:21:34 -0500 Subject: [PATCH 08/12] variable syntax for expanding to empty string when PYTHONPATH not already set --- install/install_minq.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/install_minq.sh b/install/install_minq.sh index 2a320e438..1323464f9 100644 --- a/install/install_minq.sh +++ b/install/install_minq.sh @@ -21,6 +21,6 @@ echo "Installing MINQ at ibcdfo-required commit ${MINQ_COMMIT}" git clone https://github.com/POptUS/MINQ git -C MINQ checkout "$MINQ_COMMIT" pushd MINQ/py/minq5/ -export PYTHONPATH="$PYTHONPATH:$(pwd)" +export PYTHONPATH="${PYTHONPATH:-}:$(pwd)" echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV popd From 2810c3b7e2d3066452829a3ded1afc54e65fb9ef Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 20:18:01 -0500 Subject: [PATCH 09/12] Fix project_to_target_fidelity missing d arg for botorch 0.17.2 The project() function was calling project_to_target_fidelity without the required 'd' parameter (total input dimension), which is now a required positional argument in botorch 0.17.2. --- libensemble/gen_funcs/persistent_botorch_mfkg_branin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py b/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py index 2a4081c06..a5144414f 100644 --- a/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py +++ b/libensemble/gen_funcs/persistent_botorch_mfkg_branin.py @@ -44,7 +44,7 @@ # Custom function to project posterior to target fidelity (defer to default) def project(X): - return project_to_target_fidelity(X=X, target_fidelities=target_fidelities) + return project_to_target_fidelity(X=X, target_fidelities=target_fidelities, d=3) # Wrapper function for compatibility with existing code From 1485e11cee98966e275027708402635ec7c37507 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 20:18:57 -0500 Subject: [PATCH 10/12] Fix GITHUB_ENV unbound variable in install_minq.sh GITHUB_ENV is only set in GitHub Actions, not in local pixi environments. Default to /dev/null when GITHUB_ENV is not set. --- install/install_minq.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/install_minq.sh b/install/install_minq.sh index 1323464f9..0f1fb61e3 100644 --- a/install/install_minq.sh +++ b/install/install_minq.sh @@ -22,5 +22,5 @@ git clone https://github.com/POptUS/MINQ git -C MINQ checkout "$MINQ_COMMIT" pushd MINQ/py/minq5/ export PYTHONPATH="${PYTHONPATH:-}:$(pwd)" -echo "PYTHONPATH=$PYTHONPATH" >> $GITHUB_ENV +echo "PYTHONPATH=$PYTHONPATH" >> "${GITHUB_ENV:-/dev/null}" popd From bbd492625e07b06db20eacf6213a2ef4abd1ed68 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 21:16:48 -0500 Subject: [PATCH 11/12] Fix GenSpecs outputs not including generator-defined fields like x_on_cube When using APOSMM with variables_mapping that includes x_on_cube, the GenSpecs model's outputs field was being set from VOCS-derived variables instead of from the APOSMM generator's gen_specs['out']. This caused H['x_on_cube'] to be unavailable in the History array, leading to ValueError: no field of name x_on_cube. The fix moves the generator gen_specs['out'] check into set_fields_from_vocs so it runs before the VOCS fallback. --- libensemble/specs.py | 26 +++++++++++++------------- libensemble/utils/validators.py | 10 ---------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index c95bdd8f8..5b843b125 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -14,7 +14,6 @@ check_input_dir_exists, check_inputs_exist, check_provided_ufuncs, - check_set_gen_specs_from_variables, check_valid_comms_type, check_valid_in, check_valid_out, @@ -334,15 +333,20 @@ def set_fields_from_vocs(self): if not self.inputs and self.generator is not None: self.inputs = self.persis_in - # Set outputs: variables + constants (what the generator produces) + # Set outputs: check generator.gen_specs["out"] first, then fall back to VOCS if not self.outputs: - out_fields = [] - for attr in ["variables", "constants"]: - if obj := getattr(self.vocs, attr, None): - for name, field in obj.items(): - dtype = _get_dtype(field, name) - out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) - self.outputs = out_fields + if self.generator is not None and hasattr(self.generator, "gen_specs"): + gen_out = self.generator.gen_specs.get("out", []) + if len(gen_out): + self.outputs = gen_out + if not self.outputs: + out_fields = [] + for attr in ["variables", "constants"]: + if obj := getattr(self.vocs, attr, None): + for name, field in obj.items(): + dtype = _get_dtype(field, name) + out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) + self.outputs = out_fields # Add _id field if generator returns_id is True if self.generator is not None and getattr(self.generator, "returns_id", False): @@ -375,10 +379,6 @@ def set_fields_from_vocs(self): return self - @model_validator(mode="after") - def check_set_gen_specs_from_variables(self): - return check_set_gen_specs_from_variables(self) - class AllocSpecs(BaseModel): """ diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index c1fee67ac..212b2f8bf 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -192,16 +192,6 @@ def _check_consistent_field(name, field0, field1): return values -def check_set_gen_specs_from_variables(values): - if not len(scg(values, "outputs")): - generator = scg(values, "generator") - if generator and hasattr(generator, "gen_specs"): - out = generator.gen_specs.get("out", []) - if len(out): - scs(values, "outputs", out) - return values - - def check_provided_ufuncs(self): assert hasattr(self.sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." assert isinstance(self.sim_specs.sim_f, Callable), "Simulation function is not callable." From ba594b0dc926277fc50f57b2629300796fddc8f9 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 15 Sep 2026 21:49:32 -0500 Subject: [PATCH 12/12] Merge generator gen_specs['out'] fields with VOCS-derived outputs Previously, when using APOSMA with variables_mapping, the GenSpecs outputs field was set from VOCS only, missing fields like x_on_cube, local_min, etc. that are produced by the APOSMA generator. This caused tests to fail when accessing H['x_on_cube']. The fix merges the generator's gen_specs['out'] fields into the outputs alongside the VOCS-derived fields, so the History array has all needed fields. --- libensemble/specs.py | 35 +++++++++++++++++++++------------ libensemble/utils/validators.py | 10 ++++++++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/libensemble/specs.py b/libensemble/specs.py index 5b843b125..cebaccb3d 100644 --- a/libensemble/specs.py +++ b/libensemble/specs.py @@ -14,6 +14,7 @@ check_input_dir_exists, check_inputs_exist, check_provided_ufuncs, + check_set_gen_specs_from_variables, check_valid_comms_type, check_valid_in, check_valid_out, @@ -333,20 +334,24 @@ def set_fields_from_vocs(self): if not self.inputs and self.generator is not None: self.inputs = self.persis_in - # Set outputs: check generator.gen_specs["out"] first, then fall back to VOCS + # Set outputs: variables + constants (what the generator produces) if not self.outputs: - if self.generator is not None and hasattr(self.generator, "gen_specs"): - gen_out = self.generator.gen_specs.get("out", []) - if len(gen_out): - self.outputs = gen_out - if not self.outputs: - out_fields = [] - for attr in ["variables", "constants"]: - if obj := getattr(self.vocs, attr, None): - for name, field in obj.items(): - dtype = _get_dtype(field, name) - out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) - self.outputs = out_fields + out_fields = [] + for attr in ["variables", "constants"]: + if obj := getattr(self.vocs, attr, None): + for name, field in obj.items(): + dtype = _get_dtype(field, name) + out_fields.append(_convert_dtype_to_output_tuple(name, dtype)) + self.outputs = out_fields + + # Merge in any additional fields from generator.gen_specs["out"] (e.g., x_on_cube, local_min) + if self.generator is not None and hasattr(self.generator, "gen_specs"): + gen_out = self.generator.gen_specs.get("out", []) + existing_names = {f[0] for f in self.outputs} + for field in gen_out: + if field[0] not in existing_names: + self.outputs.append(field) + existing_names.add(field[0]) # Add _id field if generator returns_id is True if self.generator is not None and getattr(self.generator, "returns_id", False): @@ -379,6 +384,10 @@ def set_fields_from_vocs(self): return self + @model_validator(mode="after") + def check_set_gen_specs_from_variables(self): + return check_set_gen_specs_from_variables(self) + class AllocSpecs(BaseModel): """ diff --git a/libensemble/utils/validators.py b/libensemble/utils/validators.py index 212b2f8bf..c1fee67ac 100644 --- a/libensemble/utils/validators.py +++ b/libensemble/utils/validators.py @@ -192,6 +192,16 @@ def _check_consistent_field(name, field0, field1): return values +def check_set_gen_specs_from_variables(values): + if not len(scg(values, "outputs")): + generator = scg(values, "generator") + if generator and hasattr(generator, "gen_specs"): + out = generator.gen_specs.get("out", []) + if len(out): + scs(values, "outputs", out) + return values + + def check_provided_ufuncs(self): assert hasattr(self.sim_specs, "sim_f"), "Simulation function not provided to SimSpecs." assert isinstance(self.sim_specs.sim_f, Callable), "Simulation function is not callable."