Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/basic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
mv libensemble/tests/.cov* .

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand All @@ -98,4 +98,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1.47.1
- uses: crate-ci/typos@v1.47.2
4 changes: 2 additions & 2 deletions .github/workflows/extra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
mv libensemble/tests/.cov* .

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

Expand All @@ -111,4 +111,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: crate-ci/typos@v1.47.1
- uses: crate-ci/typos@v1.47.2
2 changes: 1 addition & 1 deletion docs/examples/calling_scripts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions docs/platforms/aurora.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``)::
Expand Down
5 changes: 2 additions & 3 deletions docs/tutorials/aposmm_tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 4 additions & 7 deletions docs/tutorials/gpcam_tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -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
Expand Down
9 changes: 3 additions & 6 deletions docs/tutorials/xopt_bayesian_gen.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
124 changes: 115 additions & 9 deletions libensemble/ensemble.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -44,7 +53,7 @@ class Ensemble:
from libensemble import Ensemble
from libensemble.gen_classes.sampling import UniformSample
from libensemble.sim_funcs.simple_sim import norm_eval
from libensemble.specs import ExitCriteria, GenSpecs, SimSpecs
from libensemble.specs import GenSpecs, SimSpecs

sampling = Ensemble(parse_args=True)

Expand All @@ -66,10 +75,8 @@ class Ensemble:
batch_size=50,
)

sampling.exit_criteria = ExitCriteria(sim_max=100)

if __name__ == "__main__":
sampling.run()
sampling.run(sim_max=100)
sampling.save_output(__file__)

Configure by:
Expand Down Expand Up @@ -159,7 +166,7 @@ def __init__(
self,
sim_specs: SimSpecs = SimSpecs(),
gen_specs: GenSpecs = GenSpecs(),
exit_criteria: ExitCriteria = ExitCriteria(),
exit_criteria: ExitCriteria | None = None,
libE_specs: LibeSpecs = LibeSpecs(),
alloc_specs: AllocSpecs = AllocSpecs(),
persis_info: dict = {},
Expand All @@ -169,7 +176,11 @@ def __init__(
):
self.sim_specs = sim_specs
self.gen_specs = gen_specs
self.exit_criteria = exit_criteria
self._exit_criteria = ExitCriteria()
if exit_criteria is not None:
if isinstance(exit_criteria, ExitCriteria):
warnings.warn(EXIT_CRITERIA_DEPRECATION, LibEnsembleDeprecationWarning, stacklevel=2)
self._exit_criteria = exit_criteria
self._libE_specs: LibeSpecs = libE_specs
self.alloc_specs = alloc_specs
self.persis_info = persis_info
Expand All @@ -180,6 +191,7 @@ def __init__(
self.is_manager = False
self.parsed = False
self._known_comms: str = ""
self._has_run_n_evals = False

if parse_args:
self._parse_args()
Expand Down Expand Up @@ -254,7 +266,9 @@ def ready(self) -> tuple[bool, list[str]]:
):
issues.append(
"exit_criteria has no stop condition: set at least one of "
"'sim_max', 'gen_max', 'wallclock_max', or 'stop_val'."
"'sim_max', 'gen_max', 'wallclock_max', or 'stop_val' "
"either on an ExitCriteria object or directly via "
"ensemble.run(sim_max=..., gen_max=..., ...)."
)

# --- workers: must be determinable ---
Expand Down Expand Up @@ -308,13 +322,61 @@ def libE_specs(self, new_specs):

self._libE_specs.__dict__.update(**new_specs)

@property
def exit_criteria(self) -> ExitCriteria:
return self._exit_criteria

@exit_criteria.setter
def exit_criteria(self, value: ExitCriteria | None):
if isinstance(value, ExitCriteria):
warnings.warn(EXIT_CRITERIA_DEPRECATION, LibEnsembleDeprecationWarning, stacklevel=2)
self._exit_criteria = value or ExitCriteria()

def reset(self) -> None:
"""Reset the ensemble state to allow a fresh, independent run.

Clears the accumulated history (``H0``) and ``persis_info`` so that
the next :meth:`run` call starts from a clean slate — as if no
previous run had occurred.

Use this between two calls to :meth:`run` when you want **independent**
runs rather than the default history-chaining behaviour::

ens.run(sim_max=10) # first independent run
ens.reset() # clear accumulated history
ens.run(sim_max=20) # second independent run, H0 is empty again
"""
self.H0 = None
self.persis_info = {}

def _refresh_executor(self):
Executor.executor = self.executor or Executor.executor

def run(self) -> tuple[npt.NDArray, dict, int]:
def run(
self,
sim_max: int | None = None,
gen_max: int | None = None,
wallclock_max: float | None = None,
stop_val: tuple[str, float] | None = None,
) -> tuple[npt.NDArray, dict, int]:
"""
Initializes libEnsemble.

Parameters
----------
sim_max: int, Optional
Maximum number of new simulation evaluations for this run.
Overrides ``exit_criteria.sim_max`` for this call only.
gen_max: int, Optional
Maximum number of new generator calls for this run.
Overrides ``exit_criteria.gen_max`` for this call only.
wallclock_max: float, Optional
Wallclock timeout in seconds for this run.
Overrides ``exit_criteria.wallclock_max`` for this call only.
stop_val: tuple[str, float], Optional
Stop criterion ``(field, value)`` for this run.
Overrides ``exit_criteria.stop_val`` for this call only.

.. dropdown:: MPI/comms Notes

Manager--worker intercommunications are parsed from the ``comms`` key of
Expand All @@ -325,6 +387,25 @@ def run(self) -> tuple[npt.NDArray, dict, int]:
will initiate on a **duplicate** of that communicator.
Otherwise, a duplicate of ``COMM_WORLD`` will be used.

.. dropdown:: Substeps / multi-step usage

Pass exit-criteria kwargs to run a subset of an ensemble at a time.
The ensemble history (``H0``) is automatically chained across calls::

sampling = Ensemble(...)
sampling.sim_specs = SimSpecs(...)
sampling.gen_specs = GenSpecs(...)

# Run in three substeps
sampling.run(sim_max=30)
# ... adjust generator hyperparameters ...
sampling.run(sim_max=30)
sampling.run(sim_max=40)

When ``sim_max`` is used (from kwargs or ``exit_criteria``),
``libE_specs.final_gen_send`` and ``libE_specs.reuse_output_dir`` are
automatically set to ``True`` to support persistent generators across runs.

Returns
-------

Expand Down Expand Up @@ -355,16 +436,41 @@ def run(self) -> tuple[npt.NDArray, dict, int]:
raise ValueError(CHANGED_COMMS_WARN)

assert self._libE_specs is not None

# Merge kwargs into effective exit criteria for this run
run_kwargs = {
k: v
for k, v in {
"sim_max": sim_max,
"gen_max": gen_max,
"wallclock_max": wallclock_max,
"stop_val": stop_val,
}.items()
if v is not None
}
if run_kwargs:
effective_exit = self._exit_criteria.model_copy(update=run_kwargs)
self._has_run_n_evals = True
else:
effective_exit = self._exit_criteria

if sim_max is not None or getattr(self._exit_criteria, "sim_max", None) is not None:
self._libE_specs.final_gen_send = True
self._libE_specs.reuse_output_dir = True

self.H, self.persis_info, self.flag = libE(
self.sim_specs,
self.gen_specs,
self.exit_criteria,
effective_exit,
persis_info=self.persis_info,
alloc_specs=self.alloc_specs,
libE_specs=self._libE_specs,
H0=self.H0,
)

# Chain history for next call
self.H0 = self.H

return self.H, self.persis_info, self.flag

@property
Expand Down
Loading
Loading