diff --git a/README.md b/README.md index 45194fa..5f46ce1 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ Example: >>> VOCS( variables = {"x1":[0, 1], "x2":[0, 5]}, objectives = {"f1":"MAXIMIZE"}, - constants = {"alpha": 0.55}, constraints = {"c1":["LESS_THAN", 0]}, + constants = {"alpha": 0.55}, observables = {"o1"} ) ``` diff --git a/docs/conf.py b/docs/conf.py index 50d6fe9..470eefe 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,8 +69,7 @@ # General information about the project. project = "Generator_Standard" -# copyright = str(datetime.now().year) + " Argonne National Laboratory" -# author = "Jeffrey Larson, Stephen Hudson, Stefan M. Wild, David Bindel and John-Luke Navarro" +copyright = str(datetime.now().year) + " Generator Standard Authors" # today_fmt = "%B %-d, %Y" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/generator.rst b/docs/generator.rst index 2cc46e2..d34a9a4 100644 --- a/docs/generator.rst +++ b/docs/generator.rst @@ -1,6 +1,10 @@ +.. _generator: + ============= Generator API ============= .. autoclass:: generator_standard.generator.Generator :members: + :private-members: + :special-members: diff --git a/docs/index.rst b/docs/index.rst index 9f8ad88..b31e8bf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ -====================== -README / Specification -====================== +================== +Generator Standard +================== .. include:: ../README.md :parser: myst_parser.sphinx_ @@ -13,4 +13,5 @@ README / Specification :maxdepth: 2 generator - vocs \ No newline at end of file + vocs + patterns \ No newline at end of file diff --git a/docs/patterns.rst b/docs/patterns.rst new file mode 100644 index 0000000..8c5aa0c --- /dev/null +++ b/docs/patterns.rst @@ -0,0 +1,64 @@ +.. _patterns: + +===================== +Common Usage Patterns +===================== + +Within Jupyter notebook or interpreter +-------------------------------------- + +.. code-block:: python + + from my_objective import my_objective_function + from generator_standard.tests.test_generator import RandomGenerator + from generator_standard.vocs import VOCS + + vocs = VOCS(variables={"x": [0.0, 1.0]}, objectives={"f": "MINIMIZE"}) + gen = RandomGenerator(vocs) + + points = gen.suggest(10) + for point in points: + point["f"] = my_objective_function(point["x"]) + + gen.ingest(points) + +Within workflow library - libEnsemble +------------------------------------- + +`libEnsemble `_ is a Python +workflow toolkit for coordinating asynchronous and dsynamic ensembles +of calculations. It plans to support plugging in standard generators similarly +to the following: + +.. code-block:: python + + from generator_standard.tests.test_generator import RandomGenerator + from generator_standard.vocs import VOCS + + from my_objective import libE_styled_objective_function + + from libensemble import Ensemble + from libensemble.specs import GenSpecs, ExitCriteria + + vocs = VOCS(variables={"x": [0.0, 1.0]}, objectives={"f": "MINIMIZE"}) + gen = RandomGenerator(vocs) + + workflow = Ensemble() + + workflow.sim_specs = SimSpecs( + sim_f = libE_styled_objective_function, + inputs = ["x"] + outputs = [("f", float)], + ) + + workflow.gen_specs = GenSpecs( + generator=gen, + persis_in=["f"], # keep passing "f" results to the standard generator + outputs=[("x", float)], + initial_batch_size=10, + batch_size=5 + ) + + workflow.exit_criteria = ExitCriteria(sim_max=500) + + results = workflow.run() diff --git a/docs/vocs.rst b/docs/vocs.rst index 7f1abaf..f8a71a0 100644 --- a/docs/vocs.rst +++ b/docs/vocs.rst @@ -1,3 +1,5 @@ +.. _vocs: + ======== VOCS API ======== @@ -10,3 +12,5 @@ VOCS API :model-show-config-member: False :model-show-config-summary: False :member-order: bysource + :model-show-field-summary: False + :model-signature-prefix: diff --git a/generator_standard/generator.py b/generator_standard/generator.py index abe64b9..026d5c3 100644 --- a/generator_standard/generator.py +++ b/generator_standard/generator.py @@ -4,7 +4,7 @@ class Generator(ABC): """ - Tentative suggest/ingest generator interface + Each standardized generator is a Python class that inherits from this class. .. code-block:: python @@ -31,48 +31,102 @@ def finalize(self): @abstractmethod def __init__(self, vocs: VOCS, *args, **kwargs): """ - Initialize the Generator object on the user-side. Constants, class-attributes, - and preparation goes here. + The mandatory :ref:`VOCS` defines the input and output names used inside the generator. + + The constructor also accomodates variable positional and keyword arguments so each generator can be customized. + + .. code-block:: python + + >>> my_generator = MyGenerator(vocs, my_parameter, my_keyword=10) .. code-block:: python - >>> my_generator = MyGenerator(vocs, my_keyword=10) + >>> generator = NelderMead(VOCS(variables={"x": [-5.0, 5.0], "y": [-3.0, 2.0]}, objectives={"f": "MAXIMIZE"}), adaptive=False) """ self._validate_vocs(vocs) @abstractmethod def _validate_vocs(self, vocs) -> None: """ - Validate if the vocs object is compatible with the current generator. Should - raise a ValueError if the vocs object is not compatible with the generator - object + Validate if the ``VOCS`` is compatible with the current generator. Should + raise a ``ValueError`` if it is incompatible. + + .. code-block:: python + + >>> generator = NelderMead( + VOCS( + variables={"x": [-5.0, 5.0], "y": [-3.0, 2.0]}, + objectives={"f": "MAXIMIZE"}, + constraints={"c":["LESS_THAN", 0.0]} + ) + ) + + ValueError("NelderMead generator cannot accept constraints") """ @abstractmethod def suggest(self, num_points: int | None) -> list[dict]: """ - Request the next set of points to evaluate. + Returns set of points in the input space, to be evaluated next. + Each element of the list is a separate point. Keys of the dictionary include the name + of each input variable specified in the constructor. Values of the dictionaries are **scalars**. + + When ``num_points`` is passed, the generator should return exactly this number of points, or raise + a error ``ValueError`` if it is unable to. + + When ``num_points`` is not passed, the generator decides how many points to return. + Different generators will return different number of points. For instance, the simplex + would return 1 or 3 points. A genetic algorithm could return the whole population. + Batched Bayesian optimization would return the batch size (i.e., number of points that + can be processed in parallel), which would be specified in the constructor. + + In addition, some generators can generate a unique identifier for each generated point. + If implemented, this identifier should appear in the dictionary under the key ``"_id"``. + When a generator produces an identifier, it must be included in the corresponding + dictionary passed back to that generator in ``ingest`` (under the same key: ``"_id"``). .. code-block:: python >>> points = my_generator.suggest(3) >>> print(points) [{"x": 1, "y": 1}, {"x": 2, "y": 2}, {"x": 3, "y": 3}] + + >>> generator.suggest(100) # too many points + ValueError + + >>> generator.suggest() + [{"x": 1.2, "y": 0.8}, {"x": -0.2, "y": 0.4}, {"x": 4.3, "y": -0.1}] """ def ingest(self, results: list[dict]) -> None: """ - Send the results of evaluations to the generator. + Feeds data (past evaluations) to the generator. Each element of the list is a separate point. + Keys of the dictionary must include each named field specified in the ``VOCS`` provided + to the generator on instantiation. + + Any points provided to the generator via ``ingest`` that were not created by the current generator + instance should omit the ``_id`` field. If points are given to ``ingest`` with an ``_id`` value that is + not known internally, a ``ValueError`` error should be raised. .. code-block:: python >>> results = [{"x": 0.5, "y": 1.5, "f": 1}, {"x": 2, "y": 3, "f": 4}] >>> my_generator.ingest(results) + ... + >>> point = generator.suggest(1) + >>> point + [{"x": 1, "y": 1}] + >>> point["f"] = objective(point) + >>> point + [{"x": 1, "y": 1, "f": 2}] + >>> generator.ingest(point) """ def finalize(self) -> None: """ - Perform any work required to close down the generator. + **Optional**. Performs any work required to close down the generator. Some generators may need + to close down background processes, files, databases, or dump data to disk. This is similar to calling + ``.close()`` on an open file. .. code-block:: python diff --git a/generator_standard/vocs.py b/generator_standard/vocs.py index 44fb5de..681288b 100644 --- a/generator_standard/vocs.py +++ b/generator_standard/vocs.py @@ -114,6 +114,24 @@ class VOCS(BaseModel): Variables, Objectives, Constraints, and other Settings (VOCS) data structure to describe optimization problems. + Each generator accepts this object as the parameter, and must validate + that it can handle the specified set of variables, objectives, constraints, etc. + + .. code-block:: python + :linenos: + + from generator_standard.vocs import VOCS + + >>> vocs = VOCS( + variables = {"x1":[0, 1], "x2":[0, 5]}, + objectives = {"f1":"MAXIMIZE"}, + constraints = {"c1":["LESS_THAN", 0]}, + constants = {"alpha": 0.55}, + observables = {"o1"} + ) + ... + >>> my_generator = MyGenerator(vocs, parameter=100, my_keyword=10) + .. tab-set:: .. tab-item:: variables diff --git a/pyproject.toml b/pyproject.toml index 745b619..087ccb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ name = 'Campa_Generator_Standard' description = 'An effort to standardize the interface of generators in optimization libraries' readme = 'README.md' version = '0.1' +dependencies = ["pydantic"] requires-python = '>=3.10' keywords = ['optimization', 'workflows', 'generators'] classifiers = [