diff --git a/doc/source/user_guide/basic_usage/basic_setup.rst b/doc/source/user_guide/basic_usage/basic_setup.rst index 834e34a5..6bc338fb 100644 --- a/doc/source/user_guide/basic_usage/basic_setup.rst +++ b/doc/source/user_guide/basic_usage/basic_setup.rst @@ -71,11 +71,12 @@ Using an external generator ^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you have a generator from a third-party library that follows the `gest-api `_ generator standard, -you can integrate it with Optimas using -:class:`~optimas.generators.ExternalGenerator`. The external generator must be -instantiated and configured first, then passed to ``ExternalGenerator`` as a -wrapper. The external library itself must be installed separately (see -:ref:`dependencies`). +you can pass it directly to :class:`~optimas.explorations.Exploration`. The +external generator must be instantiated and configured first. The external +library itself must be installed separately (see :ref:`dependencies`). + +``ExternalGenerator`` remains available as a deprecated compatibility wrapper +for applications that need to provide Optimas-specific generator options. Known libraries containing generators compatible with this interface include `Xopt `_ and `libEnsemble @@ -85,7 +86,6 @@ Using a generic ``gest-api``-compatible generator: .. code-block:: python - from optimas.generators import ExternalGenerator from gest_api.vocs import VOCS from some_library import SomeGenerator @@ -94,14 +94,12 @@ Using a generic ``gest-api``-compatible generator: objectives={"f": "MINIMIZE"}, ) - ext_gen = SomeGenerator(vocs=vocs) - gen = ExternalGenerator(ext_gen=ext_gen, vocs=vocs) + gen = SomeGenerator(vocs=vocs) Using an `Xopt `_ generator specifically: .. code-block:: python - from optimas.generators import ExternalGenerator from gest_api.vocs import VOCS from xopt.generators.bayesian.expected_improvement import ( ExpectedImprovementGenerator, @@ -113,11 +111,8 @@ Using an `Xopt `_ generator specifically: ) # Create and (optionally) pre-seed the external generator. - ext_gen = ExpectedImprovementGenerator(vocs=vocs) - ext_gen.ingest([{"x0": 1.0, "x1": 0.5, "f": 3.2}]) - - # Wrap it for use with optimas. - gen = ExternalGenerator(ext_gen=ext_gen, vocs=vocs) + gen = ExpectedImprovementGenerator(vocs=vocs) + gen.ingest([{"x0": 1.0, "x1": 0.5, "f": 3.2}]) Evaluator diff --git a/optimas/explorations/base.py b/optimas/explorations/base.py index 22d2d074..04542755 100644 --- a/optimas/explorations/base.py +++ b/optimas/explorations/base.py @@ -17,6 +17,7 @@ from optimas.core.trial import TrialStatus from optimas.generators.base import Generator +from optimas.generators.external import adapt_generator from optimas.evaluators.base import Evaluator from optimas.evaluators.function_evaluator import FunctionEvaluator from optimas.utils.logger import get_logger @@ -104,7 +105,7 @@ def __init__( "'threads' mode is only supported when using a " "`FunctionEvaluator`. Use 'local' mode instead." ) - self.generator = generator + self.generator = adapt_generator(generator) self.evaluator = evaluator self.max_evals = max_evals self.sim_workers = sim_workers diff --git a/optimas/generators/external.py b/optimas/generators/external.py index 5b01553d..7c0a093b 100644 --- a/optimas/generators/external.py +++ b/optimas/generators/external.py @@ -5,100 +5,75 @@ (https://github.com/campa-consortium/gest-api) into Optimas. """ -from .base import Generator - - -class ExternalGenerator(Generator): - """Wrap a third-party generator that follows the ``gest-api`` standard. - - https://github.com/campa-consortium/gest-api - - Any external generator that implements this interface can be used inside - optimas by wrapping it in ``ExternalGenerator``. - - Known libraries containing generators compatible with this interface include - `Xopt `_ and `libEnsemble - `_. - - Parameters - ---------- - ext_gen : object - An object implementing/sub-classing the ``gest-api`` generator interface. The - external generator should be fully configured (including any initial - data ingested) before being passed here. The external library itself - must be installed separately. - **kwargs - Additional keyword arguments forwarded to the base - :class:`~optimas.generators.Generator` (e.g., ``vocs``, - ``save_model``). - - Examples - -------- - Using a generic ``gest-api``-compatible generator: +import warnings - .. code-block:: python +from gest_api.generator import Generator as StandardGenerator - from optimas.generators import ExternalGenerator - from gest_api.vocs import VOCS - from some_library import SomeGenerator - - vocs = VOCS( - variables={"x1": [0.0, 1.0], "x2": [0.0, 10.0]}, - objectives={"y1": "MINIMIZE"}, - ) - - ext_gen = SomeGenerator(vocs=vocs) - gen = ExternalGenerator(ext_gen=ext_gen, vocs=vocs) - - Using an `Xopt `_ generator: - - .. code-block:: python - - from optimas.generators import ExternalGenerator - from optimas.evaluators import FunctionEvaluator - from optimas.explorations import Exploration - from gest_api.vocs import VOCS - from xopt.generators.bayesian.expected_improvement import ( - ExpectedImprovementGenerator, - ) - - vocs = VOCS( - variables={"x1": [0.0, 1.0], "x2": [0.0, 10.0]}, - objectives={"y1": "MINIMIZE"}, - ) +from .base import Generator - # Create and (optionally) pre-seed the external generator. - ext_gen = ExpectedImprovementGenerator(vocs=vocs) - ext_gen.ingest([{"x1": 0.5, "x2": 5.0, "y1": 5.0}]) - # Wrap it for use with optimas. - gen = ExternalGenerator(ext_gen=ext_gen, vocs=vocs) +class _ExternalGeneratorAdapter(Generator): + """Adapt a ``gest-api`` generator to the Optimas generator protocol. - ev = FunctionEvaluator(function=my_function) - exp = Exploration(generator=gen, evaluator=ev, max_evals=20, sim_workers=4) - exp.run() + The adapter supplies Optimas trial bookkeeping while delegating the + standardized ``suggest`` and ``ingest`` operations to the wrapped object. """ def __init__( self, ext_gen, + vocs=None, **kwargs, ): - super().__init__( - **kwargs, - ) + if not isinstance(ext_gen, StandardGenerator): + raise TypeError( + "ext_gen must implement the gest-api Generator interface." + ) + if vocs is None: + try: + vocs = ext_gen.vocs + except AttributeError as exc: + raise ValueError( + "The external generator must expose a `vocs` attribute, " + "or `vocs` must be provided explicitly." + ) from exc + super().__init__(vocs=vocs, **kwargs) self.gen = ext_gen def suggest(self, n_trials): - """Request the next set of points to evaluate. - - Delegates to the wrapped generator's ``suggest`` method. - """ + """Request the next set of points to evaluate.""" return self.gen.suggest(n_trials) def ingest(self, trials): - """Send the results of evaluations to the generator. - - Delegates to the wrapped generator's ``ingest`` method. - """ + """Send the results of evaluations to the generator.""" self.gen.ingest(trials) + + +def adapt_generator(generator, **kwargs): + """Adapt a gest-api generator to the Optimas generator protocol.""" + if isinstance(generator, Generator): + return generator + if isinstance(generator, StandardGenerator): + return _ExternalGeneratorAdapter(generator, **kwargs) + raise TypeError( + "generator must be an Optimas Generator or implement the gest-api " + "Generator interface." + ) + + +class ExternalGenerator(_ExternalGeneratorAdapter): + """Deprecated compatibility wrapper for a gest-api generator. + + Pass a gest-api generator directly to :class:`~optimas.explorations.Exploration` + instead. This class remains available for compatibility and supports the + Optimas-specific generator options accepted by the previous wrapper. + """ + + def __init__(self, ext_gen, **kwargs): + warnings.warn( + "ExternalGenerator is deprecated. Pass the gest-api generator " + "directly to Exploration instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(ext_gen=ext_gen, **kwargs) diff --git a/tests/test_xopt_generators.py b/tests/test_xopt_generators.py index bb04fa76..ca8687df 100644 --- a/tests/test_xopt_generators.py +++ b/tests/test_xopt_generators.py @@ -2,7 +2,6 @@ import numpy as np -from optimas.generators import ExternalGenerator from xopt.generators.bayesian.expected_improvement import ( ExpectedImprovementGenerator, ) @@ -39,6 +38,7 @@ def test_xopt_EI(): objectives={"y1": "MINIMIZE"}, ) + # Create generator. gest-api generators are compatible. gen = ExpectedImprovementGenerator(vocs=vocs) # Create 4 initial points and ingest them @@ -50,13 +50,6 @@ def test_xopt_EI(): ] gen.ingest(initial_points) - # Create generator. - gen = ExternalGenerator( - ext_gen=gen, - vocs=vocs, - save_model=True, - ) - # Create evaluator. ev = FunctionEvaluator(function=xtest) @@ -89,6 +82,7 @@ def test_xopt_neldermead(): objectives={"y1": "MINIMIZE"}, ) + # Create generator. gest-api generators are compatible. gen = NelderMeadGenerator(vocs=vocs) # Create 4 initial points and ingest them @@ -99,13 +93,6 @@ def test_xopt_neldermead(): ] gen.ingest(initial_points) - # Create generator. - gen = ExternalGenerator( - ext_gen=gen, - vocs=vocs, - save_model=True, - ) - # Create evaluator. ev = FunctionEvaluator(function=rosenbrock)