diff --git a/README.rst b/README.rst index 246eba14bf..acde5faff9 100644 --- a/README.rst +++ b/README.rst @@ -44,8 +44,10 @@ and an exit condition. Run the following four-worker example via ``python this_f import numpy as np + from gest_api.vocs import VOCS + from libensemble import Ensemble - from libensemble.gen_funcs.sampling import uniform_random_sample + from libensemble.gen_classes.sampling import UniformSample from libensemble.sim_funcs.six_hump_camel import six_hump_camel from libensemble.specs import ExitCriteria, GenSpecs, LibeSpecs, SimSpecs @@ -53,37 +55,41 @@ and an exit condition. Run the following four-worker example via ``python this_f libE_specs = LibeSpecs(nworkers=4) + variables_objectives = VOCS( + variables={ + "x0": [-3, 3], + "x1": [-2, 2], + }, + objectives={"f": "EXPLORE"}, + ) + + generator = UniformSample(vocs=variables_objectives) + sim_specs = SimSpecs( sim_f=six_hump_camel, - inputs=["x"], - outputs=[("f", float)], + vocs=variables_objectives, ) gen_specs = GenSpecs( - gen_f=uniform_random_sample, - outputs=[("x", float, 2)], - user={ - "gen_batch_size": 50, - "lb": np.array([-3, -2]), - "ub": np.array([3, 2]), - }, + generator=generator, + vocs=variables_objectives, ) exit_criteria = ExitCriteria(sim_max=100) - sampling = Ensemble( + ensemble = Ensemble( libE_specs=libE_specs, sim_specs=sim_specs, gen_specs=gen_specs, exit_criteria=exit_criteria, ) - sampling.add_random_streams() - sampling.run() + ensemble.add_random_streams() + ensemble.run() - if sampling.is_manager: - sampling.save_output(__file__) - print("Some output data:\n", sampling.H[["x", "f"]][:10]) + if ensemble.is_manager: + ensemble.save_output(__file__) + print("Some output data:\n", ensemble.H[["x", "f"]][:10]) |Inline Example| diff --git a/docs/advanced_installation.rst b/docs/advanced_installation.rst index c02a63ed63..d002183faf 100644 --- a/docs/advanced_installation.rst +++ b/docs/advanced_installation.rst @@ -1,7 +1,7 @@ Advanced Installation ===================== -libEnsemble can be installed from ``pip``, ``Conda``, or ``Spack``. +libEnsemble can be installed from ``pip``, ``uv``, ``Conda``, or ``Spack``. libEnsemble requires the following dependencies, which are typically automatically installed alongside libEnsemble: @@ -9,15 +9,11 @@ automatically installed alongside libEnsemble: * Python_ ``>= 3.10`` * NumPy_ ``>= 1.21`` * psutil_ ``>= 5.9.4`` -* `pydantic`_ ``>= 1.10.12`` +* `pydantic`_ ``>= 2`` * pyyaml_ ``>= v6.0`` * tomli_ ``>= 1.2.1`` -Given libEnsemble's compiled dependencies, the following installation -methods each offer a trade-off between convenience and the ability -to customize builds, including platform-specific optimizations. - -We always recommend installing in a virtual environment from Conda or another source. +We recommend installing in a virtual environment from ``uv``, ``conda`` or another source. Further recommendations for selected HPC systems are given in the :ref:`HPC platform guides`. @@ -53,6 +49,12 @@ Further recommendations for selected HPC systems are given in the CC=mpicc MPICC=mpicc pip install mpi4py --no-binary mpi4py + .. tab-item:: uv + + To install the latest PyPI_ release via uv_:: + + uv pip install libensemble + .. tab-item:: conda Install libEnsemble with Conda_ from the conda-forge channel:: @@ -192,3 +194,4 @@ The following packages may be installed separately to enable additional features .. _spack_libe: https://github.com/Libensemble/spack_libe .. _tomli: https://pypi.org/project/tomli/ .. _tqdm: https://tqdm.github.io/ +.. _uv: https://docs.astral.sh/uv/ diff --git a/docs/conf.py b/docs/conf.py index 0b7e2b3dd4..f86b22abe9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -67,9 +67,9 @@ class AxParameterWarning(Warning): # Ensure it's a real warning subclass # sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath("../libensemble")) -##sys.path.append(os.path.abspath('../libensemble')) sys.path.append(os.path.abspath("../libensemble/alloc_funcs")) sys.path.append(os.path.abspath("../libensemble/gen_funcs")) +sys.path.append(os.path.abspath("../libensemble/gen_classes")) sys.path.append(os.path.abspath("../libensemble/sim_funcs")) sys.path.append(os.path.abspath("../libensemble/comms")) sys.path.append(os.path.abspath("../libensemble/utils")) diff --git a/docs/data_structures/gen_specs.rst b/docs/data_structures/gen_specs.rst index 66b12b3cc0..b3364e53f7 100644 --- a/docs/data_structures/gen_specs.rst +++ b/docs/data_structures/gen_specs.rst @@ -3,89 +3,36 @@ Generator Specs =============== -Used to specify the generator function, its inputs and outputs, and user data. - -Can be constructed and passed to libEnsemble as a Python class or a dictionary. - -.. tab-set:: - - .. tab-item:: class - - .. code-block:: python - :linenos: - - ... - import numpy as np - from libensemble import GenSpecs - from generator import gen_random_sample - - ... - - gen_specs = GenSpecs( - gen_f=gen_random_sample, - outputs=[("x", float, (1,))], - user={ - "lower": np.array([-3]), - "upper": np.array([3]), - "gen_batch_size": 5, - }, - ) - ... - - .. autopydantic_model:: libensemble.specs.GenSpecs - :model-show-json: False - :model-show-config-member: False - :model-show-config-summary: False - :model-show-validator-members: False - :model-show-validator-summary: False - :field-list-validators: False - - .. tab-item:: dict - - .. code-block:: python - :linenos: - - ... - import numpy as np - from generator import gen_random_sample - - ... - - gen_specs = { - "gen_f": gen_random_sample, - "out": [("x", float, (1,))], - "user": { - "lower": np.array([-3]), - "upper": np.array([3]), - "gen_batch_size": 5, - }, - } - - .. seealso:: - - .. _gen-specs-example1: - - - test_uniform_sampling.py_: - the generator function ``uniform_random_sample`` in sampling.py_ will generate 500 random - points uniformly over the 2D domain defined by ``gen_specs["ub"]`` and - ``gen_specs["lb"]``. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_uniform_sampling.py - :start-at: gen_specs - :end-before: end_gen_specs_rst_tag - - .. seealso:: - - - test_persistent_aposmm_nlopt.py_ shows an example where ``gen_specs["in"]`` is empty, but - ``gen_specs["persis_in"]`` specifies values to return to the persistent generator. - - - test_persistent_aposmm_with_grad.py_ shows a similar example where an ``H0`` is used to - provide points from a previous run. In this case, ``gen_specs["in"]`` is populated to provide - the generator with data for the initial points. - - - In some cases you might be able to give different (perhaps fewer) fields in ``"persis_in"`` - than ``"in"``; you may not need to give ``x`` for example, as the persistent generator - already has ``x`` for those points. See `more example uses`_ of ``persis_in``. +Used to specify the generator, its inputs and outputs, and user data. + +.. code-block:: python + :linenos: + + ... + import numpy as np + from libensemble import GenSpecs + from generator import gen_random_sample + + ... + + gen_specs = GenSpecs( + gen_f=gen_random_sample, + outputs=[("x", float, (1,))], + user={ + "lower": np.array([-3]), + "upper": np.array([3]), + "gen_batch_size": 5, + }, + ) + ... + +.. autopydantic_model:: libensemble.specs.GenSpecs + :model-show-json: False + :model-show-config-member: False + :model-show-config-summary: False + :model-show-validator-members: False + :model-show-validator-summary: False + :field-list-validators: False .. note:: diff --git a/docs/data_structures/libE_specs.rst b/docs/data_structures/libE_specs.rst index caa7b2eda8..bc2df3473a 100644 --- a/docs/data_structures/libE_specs.rst +++ b/docs/data_structures/libE_specs.rst @@ -3,18 +3,13 @@ LibE Specs ========== -libEnsemble is primarily customized by setting options within a ``LibeSpecs`` class or dictionary. +libEnsemble is primarily customized by setting options within a ``LibeSpecs`` instance. .. code-block:: python from libensemble.specs import LibeSpecs - specs = LibeSpecs( - gen_on_manager=True, - save_every_k_gens=100, - sim_dirs_make=True, - nworkers=4 - ) + specs = LibeSpecs(gen_on_manager=True, save_every_k_gens=100, sim_dirs_make=True, nworkers=4) .. dropdown:: Settings by Category :open: diff --git a/docs/data_structures/sim_specs.rst b/docs/data_structures/sim_specs.rst index 856ab5a9fe..9a023f5491 100644 --- a/docs/data_structures/sim_specs.rst +++ b/docs/data_structures/sim_specs.rst @@ -3,75 +3,32 @@ Simulation Specs ================ -Used to specify the simulation function, its inputs and outputs, and user data. +Used to specify the simulation, its inputs and outputs, and user data. -Can be constructed and passed to libEnsemble as a Python class or a dictionary. +.. code-block:: python + :linenos: -.. tab-set:: + ... + from libensemble import SimSpecs + from simulator import sim_find_sine - .. tab-item:: class + ... - .. code-block:: python - :linenos: + sim_specs = SimSpecs( + sim_f=sim_find_sine, + inputs=["x"], + outputs=[("y", float)], + user={"batch": 1234}, + ) + ... - ... - from libensemble import SimSpecs - from simulator import sim_find_sine +.. autopydantic_model:: libensemble.specs.SimSpecs + :model-show-json: False + :model-show-config-member: False + :model-show-config-summary: False + :model-show-validator-members: False + :model-show-validator-summary: False + :field-list-validators: False - ... - sim_specs = SimSpecs( - sim_f=sim_find_sine, - inputs=["x"], - outputs=[("y", float)], - user={"batch": 1234}, - ) - ... - - .. autopydantic_model:: libensemble.specs.SimSpecs - :model-show-json: False - :model-show-config-member: False - :model-show-config-summary: False - :model-show-validator-members: False - :model-show-validator-summary: False - :field-list-validators: False - - .. tab-item:: dict - - .. code-block:: python - :linenos: - - ... - from simulator import six_hump_camel - - ... - - sim_specs = { - "sim_f": six_hump_camel, - "in": ["x"], - "out": [("y", float)], - "user": {"batch": 1234}, - } - ... - - - test_uniform_sampling.py_ has a :class:`sim_specs` that declares - the name of the ``"in"`` field variable, ``"x"`` (as specified by the - corresponding generator ``"out"`` field ``"x"`` from the :ref:`gen_specs - example`). Only the field name is required in - ``sim_specs["in"]``. - - .. literalinclude:: ../../libensemble/tests/functionality_tests/test_uniform_sampling.py - :start-at: sim_specs - :end-before: end_sim_specs_rst_tag - - - run_libe_forces.py_ has a longer :class:`sim_specs` declaration with a number of - user-specific fields. These are given to the corresponding sim_f, which - can be found at forces_simf.py_. - - .. literalinclude:: ../../libensemble/tests/scaling_tests/forces/forces_adv/run_libe_forces.py - :start-at: sim_f - :end-before: end_sim_specs_rst_tag - -.. _forces_simf.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/scaling_tests/forces/forces_simple/forces_simf.py -.. _run_libe_forces.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/scaling_tests/forces/forces_simple/run_libe_forces.py .. _test_uniform_sampling.py: https://github.com/Libensemble/libensemble/blob/develop/libensemble/tests/functionality_tests/test_uniform_sampling.py diff --git a/docs/examples/gest_api.rst b/docs/examples/gest_api.rst new file mode 100644 index 0000000000..56fee4dcf4 --- /dev/null +++ b/docs/examples/gest_api.rst @@ -0,0 +1,103 @@ +============================= +(New) Standardized Generators +============================= + +libEnsemble now also supports all generators that implement the gest_api_ interface. + +.. code-block:: python + :linenos: + :emphasize-lines: 17 + + from gest_api.vocs import VOCS + from optimas.generators import GridSamplingGenerator + + from libensemble.specs import GenSpecs + + vocs = VOCS( + variables={ + "x0": [-3.0, 2.0], + "x1": [1.0, 5.0], + }, + objectives={"f": "MAXIMIZE"}, + ) + + generator = GridSamplingGenerator(vocs=vocs, n_steps=[7, 15]) + + gen_specs = GenSpecs( + generator=generator, + batch_size=4, + vocs=vocs, + ) + ... + +Included with libEnsemble +========================= + +Sampling +-------- + +.. toctree:: + :maxdepth: 1 + :caption: Sampling + :hidden: + + gest_api/sampling + +- :doc:`Basic sampling` + + Various generators for sampling a space. + +Optimization +------------ + +.. toctree:: + :maxdepth: 1 + :caption: Optimization + :hidden: + + gest_api/aposmm + +- :doc:`APOSMM` + + Asynchronously Parallel Optimization Solver for finding Multiple Minima (paper_). + +Modeling and Approximation +-------------------------- + +.. toctree:: + :maxdepth: 1 + :caption: Modeling and Approximation + :hidden: + + gest_api/gpcam + +- :doc:`gpCAM` + + Gaussian Process-based adaptive sampling using gpcam_. + +Verified Third Party +==================== + +Generators that implement the gest_api_ interface and are verified to work with libEnsemble. + +The standardized interface was developed in partnership with their authors. + +Xopt - https://github.com/xopt-org/Xopt +--------------------------------------- + +`Expected Improvement`_ + +`Nelder Mead`_ + +Optimas - https://github.com/optimas-org/optimas +------------------------------------------------ + +`Grid Sampling`_ + +.. _gest_api: https://github.com/campa-consortium/gest-api +.. _gpcam: https://gpcam.lbl.gov/ +.. _paper: https://link.springer.com/article/10.1007/s12532-017-0131-4 + +.. _Expected Improvement: https://github.com/xopt-org/Xopt/blob/v3.0/xopt/generators/bayesian/expected_improvement.py +.. _Nelder Mead: https://github.com/xopt-org/Xopt/blob/v3.0/xopt/generators/sequential/neldermead.py +.. _Grid Sampling: https://github.com/optimas-org/optimas/blob/main/optimas/generators/grid_sampling.py diff --git a/docs/examples/gest_api/aposmm.rst b/docs/examples/gest_api/aposmm.rst new file mode 100644 index 0000000000..a472ced11d --- /dev/null +++ b/docs/examples/gest_api/aposmm.rst @@ -0,0 +1,26 @@ +APOSMM +------ + +.. autoclass:: gen_classes.aposmm.APOSMM + :members: suggest, ingest, export, suggest_updates, finalize + :undoc-members: + :show-inheritance: + + +.. seealso:: + + .. tab-set:: + + .. tab-item:: APOSMM with libEnsemble + + .. literalinclude:: ../../../libensemble/tests/regression_tests/test_asktell_aposmm_nlopt.py + :linenos: + :start-at: workflow.libE_specs.gen_on_manager = True + :end-before: # Perform the run + + .. tab-item:: APOSMM standalone + + .. literalinclude:: ../../../libensemble/tests/unit_tests/test_persistent_aposmm.py + :linenos: + :start-at: def test_asktell_ingest_first(): + :end-before: assert persis_info.get("run_order"), "Standalone persistent_aposmm didn't do any localopt runs" diff --git a/docs/examples/gest_api/gpcam.rst b/docs/examples/gest_api/gpcam.rst new file mode 100644 index 0000000000..8060e29280 --- /dev/null +++ b/docs/examples/gest_api/gpcam.rst @@ -0,0 +1,21 @@ +gpCAM +------ + +.. autoclass:: gen_classes.gpCAM.GP_CAM + :members: suggest, ingest + :undoc-members: + :show-inheritance: + + +.. autoclass:: gen_classes.gpCAM.GP_CAM_Covar + :members: suggest, ingest + :undoc-members: + :show-inheritance: + + +.. seealso:: + + .. literalinclude:: ../../../libensemble/tests/regression_tests/test_asktell_gpCAM.py + :linenos: + :start-at: vocs = VOCS(variables={"x0": [-3, 3], "x1": [-2, 2], "x2": [-1, 1], "x3": [-1, 1]}, objectives={"f": "MINIMIZE"}) + :end-before: if is_manager: diff --git a/docs/examples/gest_api/sampling.rst b/docs/examples/gest_api/sampling.rst new file mode 100644 index 0000000000..9659dc6e08 --- /dev/null +++ b/docs/examples/gest_api/sampling.rst @@ -0,0 +1,10 @@ +sampling +-------- + +.. autoclass:: gen_classes.sampling.UniformSample + :members: suggest, ingest + :undoc-members: + +.. autoclass:: gen_classes.external.sampling.UniformSample + :members: suggest, ingest + :undoc-members: diff --git a/docs/index.rst b/docs/index.rst index 7182746ab0..2a2c40075e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ :maxdepth: 1 :caption: Examples: + examples/gest_api examples/gen_funcs examples/sim_funcs examples/alloc_funcs diff --git a/docs/libe_module.rst b/docs/libe_module.rst index 6f60d633a6..caa957d13f 100644 --- a/docs/libe_module.rst +++ b/docs/libe_module.rst @@ -3,19 +3,6 @@ Running an Ensemble =================== -libEnsemble features two approaches to run an ensemble. We recommend the newer ``Ensemble`` class, -but will continue to support ``libE()`` for backward compatibility. - -.. tab-set:: - - .. tab-item:: Ensemble Class - - .. autoclass:: libensemble.ensemble.Ensemble() - :members: - :no-undoc-members: - - .. tab-item:: libE() - - .. automodule:: libensemble.libE - :members: - :no-undoc-members: +.. autoclass:: libensemble.ensemble.Ensemble() + :members: + :no-undoc-members: diff --git a/docs/overview_usecases.rst b/docs/overview_usecases.rst index 6d77b197b0..2effcf8fee 100644 --- a/docs/overview_usecases.rst +++ b/docs/overview_usecases.rst @@ -1,17 +1,15 @@ Understanding libEnsemble ========================= -Manager, Workers, and User Functions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Manager, Workers, Generators, and Simulators +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. begin_overview_rst_tag libEnsemble's **manager** allocates work to **workers**, -which perform computations via **user functions**: +which perform computations via **generators** and **simulators**: -* :ref:`generator`: Generates inputs to the *simulator* (``sim_f``) -* :ref:`simulator`: Performs an evaluation based on parameters from the *generator* (``gen_f``) -* :ref:`allocator`: Decides whether a simulator or generator should be - called (and with what inputs/resources) as workers become available +* :ref:`generator`: Generates inputs to the *simulator* +* :ref:`simulator`: Performs an evaluation based on parameters from the *generator* .. figure:: images/adaptiveloop.png :alt: Adaptive loops @@ -20,10 +18,6 @@ which perform computations via **user functions**: | -The default allocator (``alloc_f``) instructs workers to run the simulator on the -highest priority work from the generator. If a worker is idle and there is -no work, that worker is instructed to call the generator. - .. figure:: images/diagram_with_persis.png :alt: libE component diagram :align: center @@ -31,12 +25,23 @@ no work, that worker is instructed to call the generator. | -An :doc:`executor` interface is available so user functions -can execute and monitor external applications. +An :doc:`executor` interface is available so generators and simulators +can launch and monitor external applications. libEnsemble uses a NumPy structured array known as the :ref:`history array` -to keep a record of all simulations. The global history array is stored on the -manager, while selected rows and fields of this array are passed to and from user functions. +to keep a record of all simulations and generated values. + +Allocator Function +~~~~~~~~~~~~~~~~~~ + +* :ref:`allocator`: Decides whether a simulator or generator should be + prompted (and with what inputs/resources) as workers become available + +The default allocator (``alloc_f``) prompts workers to run the highest priority simulator work. +If a worker is idle and there is no simulator work, that worker is prompted to query the generator. + +The default allocator is appropriate for the vast majority of use-cases, but is customizable +for users interested in more advanced allocation strategies. Example Use Cases ~~~~~~~~~~~~~~~~~ diff --git a/docs/tutorials/local_sine_tutorial.rst b/docs/tutorials/local_sine_tutorial.rst index 0509824750..7961aa2b0a 100644 --- a/docs/tutorials/local_sine_tutorial.rst +++ b/docs/tutorials/local_sine_tutorial.rst @@ -40,45 +40,36 @@ need to write a new allocation function. .. tab-item:: 2. Generator - Let's begin the coding portion of this tutorial by writing our generator function, - or :ref:`gen_f`. + Let's begin the coding portion of this tutorial by writing our generator. - An available libEnsemble worker will call this generator function with the - following parameters: - - * :ref:`InputArray`: A selection of the :ref:`History array` (*H*), - passed to the generator function in case the user wants to generate - new values based on simulation outputs. Since our generator produces random - numbers, it'll be ignored this time. - - * :ref:`persis_info`: Dictionary with worker-specific - information. In our case, this dictionary contains NumPy Random Stream objects - for generating random numbers. - - * :ref:`gen_specs`: Dictionary with user-defined static fields and - parameters. Customizable parameters such as lower and upper bounds and batch - sizes are placed within the ``gen_specs["user"]`` dictionary. - - Later on, we'll populate :class:`gen_specs` and ``persis_info`` when we initialize libEnsemble. + An available libEnsemble worker will call this generator's ``.suggest()`` method to obtain + new values to evaluate. For now, create a new Python file named ``sine_gen.py``. Write the following: - .. literalinclude:: ../../libensemble/tests/functionality_tests/sine_gen.py + .. literalinclude:: ../../libensemble/tests/functionality_tests/sine_gen_std.py :language: python :linenos: - :caption: examples/tutorials/simple_sine/sine_gen.py + :caption: examples/tutorials/simple_sine/sine_gen_std.py + + libEnsemble accepts generators that implement the gest-api_ interface. These generators + accept a ``gest_api.VOCS`` object for configuration, and contain a ``.suggest(num_points)`` + method that returns ``num_points`` points. Points consist of a list of dictionaries + with keys that match the variable names from the ``gest_api.VOCS`` object. + + Our generator's ``suggest()`` method creates ``num_points`` dictionaries. For each key in + the generator's ``self.variables``, it creates a random number uniformly distributed + between the corresponding ``lower`` and ``upper`` bounds of its domain. - Our function creates ``batch_size`` random numbers uniformly distributed - between the ``lower`` and ``upper`` bounds. A random stream - from ``persis_info`` is used to generate these values, which are then placed - into an output NumPy array that matches the dtype from ``gen_specs["out"]``. + Our generator must implement a ``_validate_vocs()`` method. Here, we implement a simple + check that ensures the ``VOCS`` object has at least one variable. .. tab-item:: 3. Simulator Next, we'll write our simulator function or :ref:`sim_f`. Simulator - functions perform calculations based on values from the generator function. - The only new parameter here is :ref:`sim_specs`, which - serves a purpose similar to the :class:`gen_specs` dictionary. + functions perform calculations based on values from the generator. + :ref:`sim_specs` is a dictionary containing user-defined fields + and parameters. Create a new Python file named ``sine_sim.py``. Write the following: @@ -88,7 +79,7 @@ need to write a new allocation function. :caption: examples/tutorials/simple_sine/sine_sim.py Our simulator function is called by a worker for every work item produced by - the generator function. This function calculates the sine of the passed value, + the generator. This function calculates the sine of the passed value, and then returns it so the worker can store the result. .. tab-item:: 4. Script @@ -97,8 +88,8 @@ need to write a new allocation function. functions and starts libEnsemble. Create an empty Python file named ``calling.py``. - In this file, we'll start by importing NumPy, libEnsemble's setup classes, - and the generator and simulator functions we just created. + In this file, we'll start by importing NumPy, libEnsemble's setup classes, the generator, + and simulator function. In a class called :ref:`LibeSpecs` we'll specify the number of workers and the manager/worker intercommunication method. @@ -272,6 +263,7 @@ need to write a new allocation function. libEnsemble use-case within our :doc:`Electrostatic Forces tutorial <./executor_forces_tutorial>`. +.. _gest-api: https://github.com/campa-consortium/gest-api .. _Matplotlib: https://matplotlib.org/ .. _MPI: https://en.wikipedia.org/wiki/Message_Passing_Interface .. _MPICH: https://www.mpich.org/ diff --git a/examples/tutorials/simple_sine/sine_gen_std.py b/examples/tutorials/simple_sine/sine_gen_std.py new file mode 100644 index 0000000000..43bafbf842 --- /dev/null +++ b/examples/tutorials/simple_sine/sine_gen_std.py @@ -0,0 +1,26 @@ +import numpy as np +from gest_api import Generator + + +class RandomSample(Generator): + """ + This sampler accepts a gest-api VOCS object for configuration and returns random samples. + """ + + def __init__(self, vocs): + self.variables = vocs.variables + self.rng = np.random.default_rng(1) + self._validate_vocs(vocs) + + def _validate_vocs(self, vocs): + if not len(vocs.variables) > 0: + raise ValueError("vocs must have at least one variable") + + def suggest(self, num_points): + output = [] + for _ in range(num_points): + trial = {} + for key in self.variables.keys(): + trial[key] = self.rng.uniform(self.variables[key].domain[0], self.variables[key].domain[1]) + output.append(trial) + return output diff --git a/libensemble/gen_classes/aposmm.py b/libensemble/gen_classes/aposmm.py index 8f77748c7b..c843d3fa1c 100644 --- a/libensemble/gen_classes/aposmm.py +++ b/libensemble/gen_classes/aposmm.py @@ -22,34 +22,35 @@ class APOSMM(PersistentGenInterfacer): `https://doi.org/10.1007/s12532-017-0131-4 `_ - VOCS variables must include both regular and *_on_cube versions. E.g.,: - - ```python - vars_std = { - "var1": [-10.0, 10.0], - "var2": [0.0, 100.0], - "var3": [1.0, 50.0], - "var1_on_cube": [0, 1.0], - "var2_on_cube": [0, 1.0], - "var3_on_cube": [0, 1.0], - } - variables_mapping = { - "x": ["var1", "var2", "var3"], - "x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], - } - gen = APOSMM(vocs, 3, 3, variables_mapping=variables_mapping, ...) - ``` + VOCS variables must include both regular and ``*_on_cube`` versions. E.g.,: + + .. code-block:: python + + vars_std = { + "var1": [-10.0, 10.0], + "var2": [0.0, 100.0], + "var3": [1.0, 50.0], + "var1_on_cube": [0, 1.0], + "var2_on_cube": [0, 1.0], + "var3_on_cube": [0, 1.0], + } + variables_mapping = { + "x": ["var1", "var2", "var3"], + "x_on_cube": ["var1_on_cube", "var2_on_cube", "var3_on_cube"], + } + gen = APOSMM(vocs, 3, 3, variables_mapping=variables_mapping, ...) Getting started --------------- APOSMM requires a minimal sample before starting optimization. A random sample across the domain - can either be retrieved via a `suggest()` call right after initialization, or the user can ingest - a set of sample points via `ingest()`. The minimal sample size is specified via the `initial_sample_size` + can either be retrieved via a ``suggest()`` call right after initialization, or the user can ingest + a set of sample points via ``ingest()``. The minimal sample size is specified via the ``initial_sample_size`` parameter. This many evaluated sample points *must* be provided to APOSMM before it will provide any local optimization points. - ```python + .. code-block:: python + # Approach 1: Retrieve sample points via suggest() gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) @@ -78,12 +79,15 @@ class APOSMM(PersistentGenInterfacer): points = gen.suggest(10) ... - ``` - *Important Note*: After the initial sample phase, APOSMM cannot accept additional "arbitrary" - sample points that are not associated with local optimization runs. - ```python + .. important:: + After the initial sample phase, APOSMM cannot accept additional "arbitrary" + sample points that are not associated with local optimization runs. + + + .. code-block:: python + gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) # ask APOSMM for some sample points @@ -99,76 +103,61 @@ class APOSMM(PersistentGenInterfacer): gen.ingest(points_from_aposmm) gen.ingest(another_sample) # THIS CRASHES - ``` Parameters ---------- - vocs: VOCS + vocs: ``VOCS`` The VOCS object, adhering to the VOCS interface from the Generator Standard. - max_active_runs: int + max_active_runs: ``int`` Bound on number of runs APOSMM is *concurrently* advancing. - initial_sample_size: int + initial_sample_size: ``int`` Minimal sample points required before starting optimization. - If `suggest(N)` is called first, APOSMM produces this many random sample points across the domain, - with N <= initial_sample_size. - - If `ingest(sample)` is called first, multiple calls like `ingest(sample)` are required until - the total number of points ingested is >= initial_sample_size. - - ```python - gen = APOSMM(vocs, max_active_runs=2, initial_sample_size=10) - - # ask APOSMM for some sample points - initial_sample = gen.suggest(10) - for point in initial_sample: - point["f"] = func(point["x"]) - gen.ingest(initial_sample) + If ``suggest(N)`` is called first, APOSMM produces this many random sample points across the domain, + with ``N <= initial_sample_size``. - # APOSMM will now provide local-optimization points. - points = gen.suggest(10) - ... - ``` + If ``ingest(sample)`` is called first, multiple calls like ``ingest(sample)`` are required until + the total number of points ingested is ``>= initial_sample_size``. - History: npt.NDArray = [] + History: ``npt.NDArray`` = ``[]`` An optional history of previously evaluated points. - sample_points: npt.NDArray = None + sample_points: ``npt.NDArray`` = ``None`` Included for compatibility with the underlying algorithm. Points to be sampled (original domain). If more sample points are needed by APOSMM during the course of the optimization, points will be drawn uniformly over the domain. - localopt_method: str = "scipy_Nelder-Mead" (scipy) or "LN_BOBYQA" (nlopt) + localopt_method: ``str`` = "scipy_Nelder-Mead" (scipy) or "LN_BOBYQA" (nlopt) The local optimization method to use. Others being added over time. - mu: float = 1e-8 + mu: ``float`` = ``1e-8`` Distance from the boundary that all localopt starting points must satisfy - nu: float = 1e-8 + nu: ``float`` = ``1e-8`` Distance from identified minima that all starting points must satisfy - rk_const: float = None + rk_const: ``float`` = ``None`` Multiplier in front of the ``r_k`` value. If not provided, it will be set to ``0.5 * ((gamma(1 + (n / 2)) * 5) ** (1 / n)) / sqrt(pi)`` - xtol_abs: float = 1e-6 + xtol_abs: ``float`` = ``1e-6`` Localopt method's convergence tolerance. - ftol_abs: float = 1e-6 + ftol_abs: ``float`` = ``1e-6`` Localopt method's convergence tolerance. - opt_return_codes: list[int] = [0] + opt_return_codes: ``list[int]`` = ``[0]`` scipy only: List of return codes that determine if a point should be ruled a local minimum. - dist_to_bound_multiple: float = 0.5 + dist_to_bound_multiple: ``float`` = ``0.5`` What fraction of the distance to the nearest boundary should the initial step size be in localopt runs. - random_seed: int = 1 + random_seed: ``int`` = ``1`` Seed for the random number generator. """ diff --git a/libensemble/gen_classes/sampling.py b/libensemble/gen_classes/sampling.py index 5e8102c22b..4304e12f15 100644 --- a/libensemble/gen_classes/sampling.py +++ b/libensemble/gen_classes/sampling.py @@ -13,6 +13,8 @@ class UniformSample(LibensembleGenerator): """ Samples over the domain specified in the VOCS. + + Implements ``suggest()`` and ``ingest()`` identically to the other generators. """ def __init__(self, VOCS: VOCS, random_seed: int = 1, *args, **kwargs): diff --git a/libensemble/tests/functionality_tests/sine_gen_std.py b/libensemble/tests/functionality_tests/sine_gen_std.py new file mode 100644 index 0000000000..43bafbf842 --- /dev/null +++ b/libensemble/tests/functionality_tests/sine_gen_std.py @@ -0,0 +1,26 @@ +import numpy as np +from gest_api import Generator + + +class RandomSample(Generator): + """ + This sampler accepts a gest-api VOCS object for configuration and returns random samples. + """ + + def __init__(self, vocs): + self.variables = vocs.variables + self.rng = np.random.default_rng(1) + self._validate_vocs(vocs) + + def _validate_vocs(self, vocs): + if not len(vocs.variables) > 0: + raise ValueError("vocs must have at least one variable") + + def suggest(self, num_points): + output = [] + for _ in range(num_points): + trial = {} + for key in self.variables.keys(): + trial[key] = self.rng.uniform(self.variables[key].domain[0], self.variables[key].domain[1]) + output.append(trial) + return output diff --git a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py index e05a49c968..f2f3eb58ae 100644 --- a/libensemble/tests/functionality_tests/test_local_sine_tutorial.py +++ b/libensemble/tests/functionality_tests/test_local_sine_tutorial.py @@ -1,5 +1,6 @@ import numpy as np -from sine_gen import gen_random_sample +from gest_api.vocs import VOCS +from sine_gen_std import RandomSample from sine_sim import sim_find_sine from libensemble import Ensemble @@ -8,14 +9,14 @@ if __name__ == "__main__": # Python-quirk required on macOS and windows libE_specs = LibeSpecs(nworkers=4, comms="local") + vocs = VOCS(variables={"x": [-3, 3]}, objectives={"y": "EXPLORE"}) # Configure our generator with this object + + generator = RandomSample(vocs) # Instantiate our generator + gen_specs = GenSpecs( - gen_f=gen_random_sample, # Our generator function - out=[("x", float, (1,))], # gen_f output (name, type, size) - user={ - "lower": np.array([-3]), # lower boundary for random sampling - "upper": np.array([3]), # upper boundary for random sampling - "gen_batch_size": 5, # number of x's gen_f generates per call - }, + generator=generator, # Pass our generator and config to libEnsemble + vocs=vocs, + batch_size=4, ) sim_specs = SimSpecs( diff --git a/pixi.lock b/pixi.lock index 0bcede09f9..e2b2a3ad86 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:590f013ffb08a53ec3d19f26f536da092df3f35fe098439e10cf1a49ac86f900 -size 1091576 +oid sha256:758bb8abc91caa2b306f539c383bf65ee184a20108d3926b9ba28281f8df32c1 +size 1091594 diff --git a/pyproject.toml b/pyproject.toml index 7165e53e23..09de832460 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -208,6 +208,7 @@ extra = [ "redis>=7.1.0,<8", ] dev = ["wat>=0.7.0,<0.8"] +docs = ["pyenchant==3.2.2", "enchant>=0.0.1,<0.0.2"] # Various config from here onward [tool.black]