From d6f3bc373bca9b163bd96599873da2e391f1e127 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 4 Jun 2026 12:01:55 -0500 Subject: [PATCH 1/7] experiments with deprecation policies applied towards older allocs --- AGENTS.md | 72 ++++++++++++++ docs/function_guides/allocator.rst | 93 ++++++++++++++----- libensemble/alloc_funcs/fast_alloc.py | 12 +++ .../alloc_funcs/fast_alloc_and_pausing.py | 11 +++ .../alloc_funcs/inverse_bayes_allocf.py | 11 +++ libensemble/alloc_funcs/only_one_gen_alloc.py | 12 +++ .../alloc_funcs/start_fd_persistent.py | 11 +++ .../start_persistent_local_opt_gens.py | 11 +++ libensemble/gen_classes/__init__.py | 1 + pyproject.toml | 7 ++ 10 files changed, 217 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f5673f64a7..d2b817631f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -99,3 +99,75 @@ When modernizing existing libEnsemble scripts (functionality tests, regression t - **Mandatory Fields**: Ensure `gen_specs["in"]` or `gen_specs["persis_in"]` includes at least one field (e.g., `["sim_id"]`) if feedback is sent back to the generator, to satisfy the allocator's requirements. - **gest-api Simulators**: The gest-api pattern also applies to simulators. Set `SimSpecs.simulator` to a callable with signature `(input_dict: dict, **kwargs) -> dict` instead of providing a `sim_f`. libEnsemble automatically wraps it with `gest_api_sim` from `libensemble.sim_funcs.gest_api_wrapper` and handles all NumPy conversions. `SimSpecs.inputs` and `SimSpecs.outputs` can be derived automatically when `SimSpecs.vocs` is provided. - **`safe_mode` is opt-in**: `libE_specs["safe_mode"]` defaults to `False`, meaning protected History fields (`gen_worker`, `gen_started_time`, `gen_ended_time`, `sim_worker`, `sim_started`, `sim_started_time`, `sim_ended`, `sim_ended_time`, `gen_informed`, `gen_informed_time`, `kill_sent`) are freely overwritable by default. Set `safe_mode=True` to enable protection. Overwriting these fields without understanding their purpose may crash libEnsemble. +- **Pre-generated samples**: Scripts that previously used the ``give_pregenerated_work`` allocator (with no generator) should be migrated to use ``PreloadedSampleGenerator`` from ``libensemble.gen_classes.preloaded``. Pass it as ``GenSpecs(generator=PreloadedSampleGenerator(H0))`` and use the default ``AllocSpecs()``. The generator serves the pre-loaded points via ``suggest()`` and returns an empty list when exhausted, triggering normal ensemble shutdown. + +Deprecation Policy +------------------ + +This section describes the standard process for deprecating **any** libEnsemble feature +(allocation functions, generator classes, public API, parameters, etc.). + +**Warning category** + +Always use ``LibEnsembleDeprecationWarning`` — a custom subclass of ``DeprecationWarning`` +importable from ``libensemble._deprecation``. Never emit bare ``DeprecationWarning`` +directly. The custom subclass lets users and downstream libraries filter libEnsemble +deprecations independently:: + + from libensemble._deprecation import LibEnsembleDeprecationWarning + warnings.filterwarnings("error", category=LibEnsembleDeprecationWarning) + +**Emit the warning** + +Emit the warning at the earliest point of use (module import, class instantiation, or +function call — whichever the user is most likely to see). Use the ``warn_deprecated()`` +helper from the same module when the standard message format is sufficient:: + + import warnings + from libensemble._deprecation import LibEnsembleDeprecationWarning + + warnings.warn( + "libensemble.. is deprecated as of libEnsemble X.Y " + "and will be removed in X.Z. Use instead. " + "See https://libensemble.readthedocs.io/... for migration guidance.", + LibEnsembleDeprecationWarning, + stacklevel=2, # points to the caller's import/call site + ) + +**Docstring banner** + +Add a ``.. deprecated:: X.Y`` directive at the top of the deprecated object's docstring, +naming the replacement and the removal version:: + + def my_old_function(...): + """ + .. deprecated:: 2.0 + ``my_old_function`` is deprecated and will be removed in libEnsemble 2.1. + Use :func:`libensemble.module.my_new_function` instead. + ... + """ + +**Sphinx docs** + +In the relevant ``.rst`` file, move the deprecated item to a "Deprecated" section (or +subsection) at the bottom of the page and prefix its ``automodule``/``autofunction`` block +with a ``.. deprecated:: X.Y`` admonition and a ``.. warning::`` summarising all items +in the section together with migration guidance. See +``docs/function_guides/allocator.rst`` for a reference example. + +**Pytest noise suppression** + +Add a ``filterwarnings`` rule to ``[tool.pytest.ini_options]`` in ``pyproject.toml`` so +that tests of deprecated-but-not-yet-removed code do not produce noisy output:: + + [tool.pytest.ini_options] + filterwarnings = [ + "ignore::libensemble._deprecation.LibEnsembleDeprecationWarning", + ] + +Remove this rule when the deprecated code is deleted. + +**Timeline** + +The standard window is: soft-deprecate in release N, hard-remove (delete code + tests) in N+1. +Tests that exclusively cover deprecated features are deleted in the removal release, not before. diff --git a/docs/function_guides/allocator.rst b/docs/function_guides/allocator.rst index 7a04f2f783..980f0c822c 100644 --- a/docs/function_guides/allocator.rst +++ b/docs/function_guides/allocator.rst @@ -15,8 +15,8 @@ We encourage experimenting with: .. dropdown:: Example - .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py - :caption: libensemble.alloc_funcs.fast_alloc.give_sim_work_first + .. literalinclude:: ../../libensemble/alloc_funcs/give_sim_work_first.py + :caption: libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first The ``alloc_f`` function definition resembles:: @@ -184,56 +184,101 @@ give_sim_work_first :language: python :linenos: -fast_alloc ----------- -.. automodule:: fast_alloc - :members: - :undoc-members: +persistent_aposmm_alloc +----------------------- +.. automodule:: persistent_aposmm_alloc + :members: + :undoc-members: + +give_pregenerated_work +---------------------- +.. automodule:: give_pregenerated_work + :members: + :undoc-members: -.. dropdown:: :underline:`fast_alloc.py` +.. _deprecated-alloc-label: - .. literalinclude:: ../../libensemble/alloc_funcs/fast_alloc.py - :language: python - :linenos: +Deprecated Allocation Functions +================================ -start_persistent_local_opt_gens -------------------------------- -.. automodule:: start_persistent_local_opt_gens +.. warning:: + + The following allocation functions are **deprecated as of libEnsemble 2.0** and will be + **removed in libEnsemble 2.1**. They emit a :class:`~libensemble._deprecation.LibEnsembleDeprecationWarning` + on import. + + **Migration guidance:** + + - Functions that managed non-persistent generators (``fast_alloc``, ``fast_alloc_and_pausing``, + ``only_one_gen_alloc``) should be replaced with + :func:`~libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first` or the default + :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` with a + persistent generator. + - APOSMM-adjacent functions (``start_persistent_local_opt_gens``, ``start_fd_persistent``) + should migrate to + :func:`~libensemble.alloc_funcs.persistent_aposmm_alloc.persistent_aposmm_alloc`. + - ``inverse_bayes_allocf`` should be replaced with the default ``only_persistent_gens`` + combined with a persistent generator that implements the required batch/subbatch logic. + +fast_alloc +---------- +.. deprecated:: 2.0 + Use :func:`~libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first` or the default + :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` instead. + Will be removed in libEnsemble 2.1. + +.. automodule:: fast_alloc :members: :undoc-members: fast_alloc_and_pausing ---------------------- +.. deprecated:: 2.0 + Use the default :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` + with a persistent generator instead. Will be removed in libEnsemble 2.1. + .. automodule:: fast_alloc_and_pausing :members: :undoc-members: only_one_gen_alloc ------------------ +.. deprecated:: 2.0 + Use :func:`~libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first` with + ``num_active_gens=1``, or the default + :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` instead. + Will be removed in libEnsemble 2.1. + .. automodule:: only_one_gen_alloc :members: :undoc-members: start_fd_persistent ------------------- +.. deprecated:: 2.0 + Use the default :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` + with a persistent generator instead. Will be removed in libEnsemble 2.1. + .. automodule:: start_fd_persistent :members: :undoc-members: -persistent_aposmm_alloc ------------------------ -.. automodule:: persistent_aposmm_alloc - :members: - :undoc-members: +start_persistent_local_opt_gens +------------------------------- +.. deprecated:: 2.0 + Use :func:`~libensemble.alloc_funcs.persistent_aposmm_alloc.persistent_aposmm_alloc` + instead. Will be removed in libEnsemble 2.1. -give_pregenerated_work ----------------------- -.. automodule:: give_pregenerated_work - :members: - :undoc-members: +.. automodule:: start_persistent_local_opt_gens + :members: + :undoc-members: inverse_bayes_allocf -------------------- +.. deprecated:: 2.0 + Use the default :func:`~libensemble.alloc_funcs.start_only_persistent.only_persistent_gens` + with a persistent generator instead. Will be removed in libEnsemble 2.1. + .. automodule:: inverse_bayes_allocf :members: :undoc-members: diff --git a/libensemble/alloc_funcs/fast_alloc.py b/libensemble/alloc_funcs/fast_alloc.py index e2027da1c0..f55a9464ac 100644 --- a/libensemble/alloc_funcs/fast_alloc.py +++ b/libensemble/alloc_funcs/fast_alloc.py @@ -1,8 +1,20 @@ +from libensemble._deprecation import warn_deprecated from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.fast_alloc", + replacement="libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first " + "or the default only_persistent_gens with a persistent generator", +) + def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``fast_alloc.give_sim_work_first`` is deprecated and will be removed in libEnsemble 2.1. + Use :func:`libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first` or the + default ``only_persistent_gens`` (with a persistent generator) instead. + This allocation function gives (in order) entries in ``H`` to idle workers to evaluate in the simulation function. The fields in ``sim_specs["in"]`` are given. If all entries in `H` have been given a be evaluated, a worker diff --git a/libensemble/alloc_funcs/fast_alloc_and_pausing.py b/libensemble/alloc_funcs/fast_alloc_and_pausing.py index fd162a6623..a7a3b84424 100644 --- a/libensemble/alloc_funcs/fast_alloc_and_pausing.py +++ b/libensemble/alloc_funcs/fast_alloc_and_pausing.py @@ -1,10 +1,21 @@ import numpy as np +from libensemble._deprecation import warn_deprecated from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.fast_alloc_and_pausing", + replacement="the default only_persistent_gens with a persistent generator", +) + def give_sim_work_first(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``fast_alloc_and_pausing.give_sim_work_first`` is deprecated and will be removed in + libEnsemble 2.1. Use the default ``only_persistent_gens`` (with a persistent generator) + instead. + This allocation function gives (in order) entries in ``H`` to idle workers to evaluate in the simulation function. The fields in ``sim_specs["in"]`` are given. If all entries in `H` have been given a be evaluated, a worker diff --git a/libensemble/alloc_funcs/inverse_bayes_allocf.py b/libensemble/alloc_funcs/inverse_bayes_allocf.py index e0521df6ff..2daf3e84b2 100644 --- a/libensemble/alloc_funcs/inverse_bayes_allocf.py +++ b/libensemble/alloc_funcs/inverse_bayes_allocf.py @@ -1,11 +1,22 @@ import numpy as np +from libensemble._deprecation import warn_deprecated from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.inverse_bayes_allocf", + replacement="the default only_persistent_gens with a persistent generator", +) + def only_persistent_gens_for_inverse_bayes(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``inverse_bayes_allocf.only_persistent_gens_for_inverse_bayes`` is deprecated and will + be removed in libEnsemble 2.1. Use the default ``only_persistent_gens`` (with a + persistent generator) instead. + Starts up to gen_count number of persistent generators. These persistent generators produce points (x) in batches and subbatches. The points x are given in subbatches to workers to perform a calculation. diff --git a/libensemble/alloc_funcs/only_one_gen_alloc.py b/libensemble/alloc_funcs/only_one_gen_alloc.py index 7eb6a91e0b..b00c1296bc 100644 --- a/libensemble/alloc_funcs/only_one_gen_alloc.py +++ b/libensemble/alloc_funcs/only_one_gen_alloc.py @@ -1,8 +1,20 @@ +from libensemble._deprecation import warn_deprecated from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.only_one_gen_alloc", + replacement="libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first " + "with num_active_gens=1, or the default only_persistent_gens", +) + def ensure_one_active_gen(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``only_one_gen_alloc.ensure_one_active_gen`` is deprecated and will be removed in + libEnsemble 2.1. Use :func:`libensemble.alloc_funcs.give_sim_work_first.give_sim_work_first` + with ``num_active_gens=1``, or the default ``only_persistent_gens`` instead. + This allocation function gives (in order) entries in ``H`` to idle workers to evaluate in the simulation function. The fields in ``sim_specs["in"]`` are given. If there is no active generator, then one is started. diff --git a/libensemble/alloc_funcs/start_fd_persistent.py b/libensemble/alloc_funcs/start_fd_persistent.py index 36fba0a730..56f4155894 100644 --- a/libensemble/alloc_funcs/start_fd_persistent.py +++ b/libensemble/alloc_funcs/start_fd_persistent.py @@ -1,11 +1,22 @@ import numpy as np +from libensemble._deprecation import warn_deprecated from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.start_fd_persistent", + replacement="the default only_persistent_gens with a persistent generator", +) + def finite_diff_alloc(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``start_fd_persistent.finite_diff_alloc`` is deprecated and will be removed in + libEnsemble 2.1. Use the default ``only_persistent_gens`` (with a persistent generator) + instead. + This allocation function will give simulation work if possible, but otherwise start 1 persistent generator. If all points requested by the persistent generator for a given (x_ind, f_ind) pair have been returned from the diff --git a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py index 9f0537b8e3..0e7ff2f57d 100644 --- a/libensemble/alloc_funcs/start_persistent_local_opt_gens.py +++ b/libensemble/alloc_funcs/start_persistent_local_opt_gens.py @@ -1,12 +1,23 @@ import numpy as np +from libensemble._deprecation import warn_deprecated from libensemble.gen_funcs.persistent_aposmm import decide_where_to_start_localopt, extract_rk_c, update_history_dist from libensemble.message_numbers import EVAL_GEN_TAG from libensemble.tools.alloc_support import AllocSupport, InsufficientFreeResources +warn_deprecated( + name="libensemble.alloc_funcs.start_persistent_local_opt_gens", + replacement="libensemble.alloc_funcs.persistent_aposmm_alloc.persistent_aposmm_alloc", +) + def start_persistent_local_opt_gens(W, H, sim_specs, gen_specs, alloc_specs, persis_info, libE_info): """ + .. deprecated:: 2.0 + ``start_persistent_local_opt_gens.start_persistent_local_opt_gens`` is deprecated and + will be removed in libEnsemble 2.1. Use + :func:`libensemble.alloc_funcs.persistent_aposmm_alloc.persistent_aposmm_alloc` instead. + This allocation function will do the following: - Start up a persistent generator that is a local opt run at the first point diff --git a/libensemble/gen_classes/__init__.py b/libensemble/gen_classes/__init__.py index d0524159da..339e192102 100644 --- a/libensemble/gen_classes/__init__.py +++ b/libensemble/gen_classes/__init__.py @@ -1,2 +1,3 @@ from .aposmm import APOSMM # noqa: F401 +from .preloaded import PreloadedSampleGenerator # noqa: F401 from .sampling import UniformSample # noqa: F401 diff --git a/pyproject.toml b/pyproject.toml index fbb96f22da..7ad08176e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -217,6 +217,13 @@ dev = ["wat>=0.7.0,<0.8"] docs = ["pyenchant", "enchant>=0.0.1,<0.0.2", "sphinx-lfs-content>=1.1.10,<2"] # Various config from here onward +[tool.pytest.ini_options] +filterwarnings = [ + # Suppress deprecation warnings from alloc modules being removed in 2.1. + # Their tests are deleted in 2.1; until then, silence the import-time noise. + "ignore::libensemble._deprecation.LibEnsembleDeprecationWarning", +] + [tool.black] line-length = 120 target-version = ["py311", "py312", "py313", "py314"] From a6fd18a16643afa62fe59183d2ca4970b0dae701 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Jun 2026 11:51:21 -0500 Subject: [PATCH 2/7] adds _deprecation module. Adds PreloadedGenerator class for existing work without needing a corresponding alloc --- libensemble/_deprecation.py | 38 ++++++ libensemble/gen_classes/preloaded.py | 175 +++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 libensemble/_deprecation.py create mode 100644 libensemble/gen_classes/preloaded.py diff --git a/libensemble/_deprecation.py b/libensemble/_deprecation.py new file mode 100644 index 0000000000..4606bb06dc --- /dev/null +++ b/libensemble/_deprecation.py @@ -0,0 +1,38 @@ +""" +Deprecation utilities for libEnsemble. +""" + +import warnings + + +class LibEnsembleDeprecationWarning(DeprecationWarning): + """Warning category for deprecated libEnsemble features. + + Subclass of :class:`DeprecationWarning` so users can filter libEnsemble + deprecations independently:: + + import warnings + from libensemble._deprecation import LibEnsembleDeprecationWarning + warnings.filterwarnings("error", category=LibEnsembleDeprecationWarning) + """ + + +def warn_deprecated(name: str, replacement: str, removal_version: str = "2.1") -> None: + """Emit a :class:`LibEnsembleDeprecationWarning` for a deprecated feature. + + Parameters + ---------- + name: + Dotted module or object path (e.g. ``"libensemble.alloc_funcs.fast_alloc"``). + replacement: + Human-readable description of the recommended replacement. + removal_version: + The libEnsemble version in which the feature will be removed. + """ + warnings.warn( + f"{name} is deprecated as of libEnsemble 2.0 " + f"and will be removed in {removal_version}. " + f"Use {replacement} instead.", + LibEnsembleDeprecationWarning, + stacklevel=3, + ) diff --git a/libensemble/gen_classes/preloaded.py b/libensemble/gen_classes/preloaded.py new file mode 100644 index 0000000000..d032e205e4 --- /dev/null +++ b/libensemble/gen_classes/preloaded.py @@ -0,0 +1,175 @@ +"""Generator class for evaluating a pre-existing sample of points. + +This module provides :class:`PreloadedSampleGenerator`, a gest-api compatible +generator that wraps a user-supplied set of points and serves them to libEnsemble +workers for simulation evaluation. No VOCS or online generation is required. + +Typical usage:: + + import numpy as np + from libensemble import Ensemble + from libensemble.gen_classes.preloaded import PreloadedSampleGenerator + from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs + + H0 = np.zeros(500, dtype=[("x", float, 8), ("sim_id", int)]) + H0["x"] = my_existing_points + H0["sim_id"] = range(500) + + 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() + +This replaces the legacy ``give_pregenerated_work`` allocator pattern, which +required a custom ``AllocSpecs`` and bypassed the generator entirely. With +:class:`PreloadedSampleGenerator` the default ``only_persistent_gens`` allocator +is used transparently. +""" + +from typing import List, Optional, Union + +import numpy as np +import numpy.typing as npt +from gest_api import Generator +from gest_api.vocs import VOCS + +from libensemble.utils.misc import np_to_list_dicts + +__all__ = ["PreloadedSampleGenerator"] + +# Sentinel VOCS used when the caller does not supply one. The generator never +# actually samples from this; it is required only to satisfy the gest-api +# Generator base class constructor. The list form ``[low, high]`` is the +# canonical gest-api shorthand for a continuous variable. +_SENTINEL_VOCS = VOCS(variables={"_preloaded": [0.0, 1.0]}) + + +class PreloadedSampleGenerator(Generator): + """A gest-api generator that serves a fixed, pre-existing set of points. + + Points are taken from a numpy structured array (or a list of dicts) supplied + at construction time and returned to libEnsemble in chunks via :meth:`suggest`. + Once all points are exhausted :meth:`suggest` returns an empty list, which + causes the default ``only_persistent_gens`` allocator to shut down the + generator and end the ensemble. + + No online learning or VOCS sampling is performed; :meth:`ingest` is a no-op. + + Parameters + ---------- + points: + Pre-generated points to evaluate. May be either: + + * A numpy structured array whose field names match ``sim_specs["in"]``. + Fields ``sim_id`` and ``sim_started`` are ignored (libEnsemble manages + them internally). + * A list of dicts with consistent keys. + vocs: + Optional VOCS object. If omitted a sentinel placeholder is used — the + generator does not sample from it. + batch_size: + Number of points to return per :meth:`suggest` call. Defaults to + returning all remaining points at once (i.e. one large batch). Setting + this to a positive integer enables streaming delivery, which can reduce + peak memory pressure for very large pre-generated samples. + + Examples + -------- + Evaluate 1000 pre-generated borehole inputs: + + .. code-block:: python + + import numpy as np + 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 + + n_samp = 1000 + pts = np.zeros(n_samp, dtype=[("x", float, 8)]) + pts["x"] = gen_borehole_input(n_samp) + + 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() + """ + + def __init__( + self, + points: Union[npt.NDArray, List[dict]], + vocs: Optional[VOCS] = None, + batch_size: Optional[int] = None, + ) -> None: + super().__init__(vocs if vocs is not None else _SENTINEL_VOCS) + + # Normalise to a list-of-dicts for the gest-api suggest() return type. + if isinstance(points, np.ndarray): + # Strip fields that libEnsemble manages internally before converting. + internal = {"sim_id", "sim_started", "sim_ended", "gen_worker"} + user_fields = [n for n in points.dtype.names if n not in internal] + self._points: List[dict] = np_to_list_dicts(points[user_fields]) + else: + self._points = list(points) + + if batch_size is not None and batch_size <= 0: + raise ValueError(f"batch_size must be a positive integer, got {batch_size!r}") + self._batch_size = batch_size + self._cursor: int = 0 + + # ------------------------------------------------------------------ + # gest-api interface + # ------------------------------------------------------------------ + + def _validate_vocs(self, vocs: VOCS) -> None: # noqa: D102 + pass # No VOCS constraints — pre-loaded points already exist. + + def suggest(self, n_trials: int) -> List[dict]: + """Return the next batch of pre-loaded points. + + Parameters + ---------- + n_trials: + Hint from the allocator for how many points are needed. When + ``batch_size`` was set at construction that value takes precedence; + otherwise ``n_trials`` is honoured. + + Returns + ------- + list[dict] + Next chunk of points, or ``[]`` when all points have been served. + """ + if self._cursor >= len(self._points): + return [] + + # Determine how many points to emit this call. + chunk = self._batch_size if self._batch_size is not None else n_trials + # Never return more than what is left. + chunk = min(chunk, len(self._points) - self._cursor) + + batch = self._points[self._cursor : self._cursor + chunk] + self._cursor += chunk + return batch + + def ingest(self, calc_in: List[dict]) -> None: # noqa: D102 — intentional no-op + """Receive simulation results (no-op — pre-loaded sample needs no feedback).""" + + # ------------------------------------------------------------------ + # Convenience + # ------------------------------------------------------------------ + + @property + def n_remaining(self) -> int: + """Number of points not yet served by :meth:`suggest`.""" + return max(0, len(self._points) - self._cursor) + + def __repr__(self) -> str: # pragma: no cover + return ( + f"{self.__class__.__name__}(" + f"total={len(self._points)}, " + f"served={self._cursor}, " + f"remaining={self.n_remaining}, " + f"batch_size={self._batch_size!r})" + ) From 2d4e64dab7f536110758aa6a99d4cbeda939b331 Mon Sep 17 00:00:00 2001 From: jlnav Date: Fri, 5 Jun 2026 13:37:37 -0500 Subject: [PATCH 3/7] 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 | 179 ++++++++++++++++++ 2 files changed, 275 insertions(+), 5 deletions(-) diff --git a/libensemble/ensemble.py b/libensemble/ensemble.py index 24b47d72b0..b0b4544ca3 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 0e5de32239..f57a013cb1 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -270,6 +270,178 @@ def test_ready_happy_path(): assert issues == [], f"Issues should be empty but got: {issues}" +# --- run() kwargs / substep tests --- + + +# --- 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 H0 sim_ended count 5 but got {sim_count}" + + if __name__ == "__main__": test_ensemble_init() test_ensemble_parse_args_false() @@ -283,3 +455,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() From dd962afcbba62592ddd6585b4b691417ab0243af Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jun 2026 14:30:24 -0500 Subject: [PATCH 4/7] 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 | 8 ++-- .../test_GPU_variable_resources.py | 13 +++---- .../test_GPU_variable_resources_multi_task.py | 6 +-- .../test_asktell_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 | 39 ++++++++----------- 44 files changed, 147 insertions(+), 225 deletions(-) diff --git a/docs/examples/calling_scripts.rst b/docs/examples/calling_scripts.rst index 7a58ad05e4..269fe1d3cf 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 c29ed0bc08..a2f652f3ae 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 7cc0d7b9fd..094a7c1f63 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 190697374b..959ef22f24 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 9227ac8ce1..bbdf756d91 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 b0b4544ca3..63365cbc2a 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 d032e205e4..02e5a9b38a 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 90dab75070..9042244146 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 463fbbb0e9..b50df3a23d 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 3e37bc86dc..f90ad54357 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,10 +55,9 @@ 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) 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 d6b368b93d..d33316b2d3 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 2a6cda15bb..77aca1d5db 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 3c6fa0b323..4abffc5a26 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 286a0ecdc4..8c794298c1 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 988fe0e78c..2b54d37c5e 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 f58620b90f..7edaa1eaeb 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 456b3ca601..3bbccdeadb 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 c26a662010..d3c783d75c 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 f535e17096..563a951624 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 c8455b459c..357184a2aa 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 5564e7f968..38fb327f0b 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 2a68d6e722..1427edcf04 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 8f1ee674ce..0c3e699117 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 60e43fa57e..f61689f4f8 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 72fa6eed08..23a1df9ad3 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 fb5b75c321..62ce184db6 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 08d97ed6b6..cd5ca06156 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 b9fbbb34b6..b91a18bebf 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 5ec670aa95..78d9ec2edd 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 c0f2cff170..b0c7f4b6ab 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 3820bfdec8..b2f3b6fef8 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 22e4472868..50b29730e9 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 7fb158b58b..f45d58d265 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 d89de94493..8394308c84 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 c7db6d363a..08e02b6ec2 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 13939169f7..218ce5e90f 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 30d0952077..288e45824f 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 7b45ebd574..4b4513ea9d 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 09a43e175f..1f0ad675e8 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 e5faeb9b49..b71c8ce509 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 a337a09e43..8c99fe2ddb 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 9c066ec932..189443b9b6 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 2d23c83a88..c3e5024f77 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 f57a013cb1..9de46c2f7b 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,14 +255,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}" @@ -273,9 +271,6 @@ def test_ready_happy_path(): # --- run() kwargs / substep tests --- -# --- 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 @@ -337,7 +332,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( @@ -350,9 +345,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 83020debbf65cf2563b1f3ebc99b4df8ca3073e4 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jun 2026 15:24:05 -0500 Subject: [PATCH 5/7] 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 f90ad54357..a3d7da3605 100644 --- a/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py +++ b/libensemble/tests/functionality_tests/test_evaluate_existing_plus_gen.py @@ -55,13 +55,14 @@ def create_H0(gen_specs, H0_size): }, } sampling.gen_specs = GenSpecs(**gen_specs) - 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(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 c1e1345161d9b592dd13eea85b0359b8816ab9a3 Mon Sep 17 00:00:00 2001 From: jlnav Date: Thu, 11 Jun 2026 15:55:32 -0500 Subject: [PATCH 6/7] run_tests.py ignores deprecationwarnings --- libensemble/tests/run_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libensemble/tests/run_tests.py b/libensemble/tests/run_tests.py index e566d451f5..31798c7f11 100755 --- a/libensemble/tests/run_tests.py +++ b/libensemble/tests/run_tests.py @@ -303,7 +303,7 @@ def skip_config(directives, args, comm): def make_run_line(python_exec, test_script, comm, nprocs, args): """Build run line""" - cmd = python_exec + cov_opts + [test_script] + cmd = python_exec + ["-W", "ignore::DeprecationWarning"] + cov_opts + [test_script] if comm == "mpi": cmd = ["mpiexec", "-np", str(nprocs)] + (args.a.split() if args.a else []) + cmd else: From 999a3b9930e8a81e8e0889ae17bb71ca62a18a44 Mon Sep 17 00:00:00 2001 From: jlnav Date: Tue, 14 Jul 2026 14:05:06 -0500 Subject: [PATCH 7/7] 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 63365cbc2a..ab6c281ebc 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 9de46c2f7b..c3999b74d3 100644 --- a/libensemble/tests/unit_tests/test_ensemble.py +++ b/libensemble/tests/unit_tests/test_ensemble.py @@ -385,7 +385,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 @@ -404,6 +404,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