From 5951d502d026164278904ffd340558bb6cdd3b7d Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 18:14:53 -0500 Subject: [PATCH 01/10] Convert to Ax v1 API --- optimas/generators/ax/developer/ax_metric.py | 2 +- optimas/generators/ax/developer/multitask.py | 69 +++++++++---------- .../ax/import_error_dummy_generator.py | 2 +- optimas/generators/ax/service/ax_client.py | 9 +-- optimas/generators/ax/service/base.py | 36 +++++----- .../generators/ax/service/multi_fidelity.py | 49 +++++++------ .../generators/ax/service/single_fidelity.py | 12 ++-- optimas/utils/ax/ax_model_manager.py | 22 +++--- pyproject.toml | 4 +- tests/test_ax_generators.py | 15 ++-- 10 files changed, 111 insertions(+), 109 deletions(-) diff --git a/optimas/generators/ax/developer/ax_metric.py b/optimas/generators/ax/developer/ax_metric.py index 6e51802b..dc98cd2d 100644 --- a/optimas/generators/ax/developer/ax_metric.py +++ b/optimas/generators/ax/developer/ax_metric.py @@ -1,7 +1,7 @@ """Contains the definition of the Ax metric used for multitask optimization.""" import pandas as pd -from ax import Metric +from ax.core.metric import Metric from ax.core.batch_trial import BatchTrial from ax.core.data import Data from ax.utils.common.result import Ok diff --git a/optimas/generators/ax/developer/multitask.py b/optimas/generators/ax/developer/multitask.py index 33582c05..b49dcdb8 100644 --- a/optimas/generators/ax/developer/multitask.py +++ b/optimas/generators/ax/developer/multitask.py @@ -16,45 +16,33 @@ from ax.core.optimization_config import OptimizationConfig from ax.core.objective import Objective as AxObjective from ax.runners import SyntheticRunner -from ax.modelbridge.factory import get_sobol -from ax.modelbridge.torch import TorchModelBridge +from ax.adapter.factory import get_sobol +from ax.adapter.torch import TorchAdapter from ax.core.observation import ObservationFeatures from ax.core.generator_run import GeneratorRun from ax.storage.json_store.save import save_experiment from ax.storage.metric_registry import register_metrics -from ax.modelbridge.registry import Models, ST_MTGP_trans - -try: - # For Ax >= 0.5.0 - from ax.modelbridge.transforms.derelativize import Derelativize - from ax.modelbridge.transforms.convert_metric_names import ( - ConvertMetricNames, - ) - from ax.modelbridge.transforms.trial_as_task import TrialAsTask - from ax.modelbridge.transforms.stratified_standardize_y import ( - StratifiedStandardizeY, - ) - from ax.modelbridge.transforms.task_encode import TaskChoiceToIntTaskChoice - from ax.modelbridge.registry import MBM_X_trans - - MT_MTGP_trans = MBM_X_trans + [ - Derelativize, - ConvertMetricNames, - TrialAsTask, - StratifiedStandardizeY, - TaskChoiceToIntTaskChoice, - ] +from ax.adapter.registry import Generators, MBM_X_trans +from ax.adapter.transforms.derelativize import Derelativize +from ax.adapter.transforms.metrics_as_task import MetricsAsTask +from ax.adapter.transforms.trial_as_task import TrialAsTask +from ax.adapter.transforms.stratified_standardize_y import ( + StratifiedStandardizeY, +) +from ax.adapter.transforms.task_encode import TaskChoiceToIntTaskChoice +from ax.adapter.registry import ST_MTGP_trans -except ImportError: - # For Ax < 0.5.0 - from ax.modelbridge.registry import MT_MTGP_trans +MT_MTGP_trans = MBM_X_trans + [ + Derelativize, + MetricsAsTask, + TrialAsTask, + StratifiedStandardizeY, + TaskChoiceToIntTaskChoice, +] from ax.core.experiment import Experiment from ax.core.data import Data -from ax.modelbridge.transforms.convert_metric_names import ( - tconfig_from_mt_experiment, -) from optimas.generators.ax.base import AxGenerator from optimas.core import ( @@ -81,7 +69,7 @@ def get_MTGP( trial_index: Optional[int] = None, device: torch.device = torch.device("cpu"), dtype: torch.dtype = torch.double, -) -> TorchModelBridge: +) -> TorchAdapter: """Instantiate a Multi-task Gaussian Process (MTGP) model. Points are generated with EI (Expected Improvement). @@ -94,11 +82,21 @@ def get_MTGP( t.index: t.trial_type for t in experiment.trials.values() } transforms = MT_MTGP_trans + + # Build MetricsAsTask config manually (replaces tconfig_from_mt_experiment) + canonical = experiment._metric_to_canonical_name + metric_task_map = {} + for metric_name, canonical_name in canonical.items(): + if metric_name != canonical_name: + if canonical_name not in metric_task_map: + metric_task_map[canonical_name] = [] + metric_task_map[canonical_name].append(metric_name) + transform_configs = { "TrialAsTask": { "trial_level_map": {"trial_type": trial_index_to_type} }, - "ConvertMetricNames": tconfig_from_mt_experiment(experiment), + "MetricsAsTask": {"metric_task_map": metric_task_map}, } else: # Set transforms for a Single-type MTGP model. @@ -125,7 +123,7 @@ def get_MTGP( ) return assert_is_instance( - Models.ST_MTGP( + Generators.ST_MTGP( experiment=experiment, search_space=search_space or experiment.search_space, data=data, @@ -135,7 +133,7 @@ def get_MTGP( torch_device=device, status_quo_features=status_quo_features, ), - TorchModelBridge, + TorchAdapter, ) @@ -354,7 +352,6 @@ def _incorporate_external_data(self, trials: List[Trial]) -> None: arms.append( Arm(parameters=params, name=param_to_name[arm.signature]) ) - # self._next_id += 1 # Create new batch trial. gr = GeneratorRun(arms=arms, weights=[1.0] * len(arms)) @@ -597,7 +594,7 @@ def _save_model_to_file(self) -> None: def max_utility_from_GP( - n: int, m: TorchModelBridge, gr: GeneratorRun, hifi_task: str + n: int, m: TorchAdapter, gr: GeneratorRun, hifi_task: str ) -> GeneratorRun: """Select the max utility points according to the MTGP predictions. diff --git a/optimas/generators/ax/import_error_dummy_generator.py b/optimas/generators/ax/import_error_dummy_generator.py index 2e57ed6a..f4c57b23 100644 --- a/optimas/generators/ax/import_error_dummy_generator.py +++ b/optimas/generators/ax/import_error_dummy_generator.py @@ -12,5 +12,5 @@ def __init__(self, *args, **kwargs) -> None: raise RuntimeError( "You need to install ax-platform, in order " "to use Ax-based generators in optimas.\n" - "e.g. with `pip install 'ax-platform<1.0.0'`" + "e.g. with `pip install 'ax-platform'`" ) diff --git a/optimas/generators/ax/service/ax_client.py b/optimas/generators/ax/service/ax_client.py index 3ea5194f..c52be2f8 100644 --- a/optimas/generators/ax/service/ax_client.py +++ b/optimas/generators/ax/service/ax_client.py @@ -134,8 +134,9 @@ def _create_ax_client(self) -> AxClient: def _use_cuda(self, ax_client: AxClient): """Determine whether the AxClient uses CUDA.""" - for step in ax_client.generation_strategy._steps: - if "torch_device" in step.model_kwargs: - if step.model_kwargs["torch_device"] == "cuda": - return True + for node in ax_client.generation_strategy._nodes: + for gs in node.generator_specs: + if "torch_device" in gs.generator_kwargs: + if gs.generator_kwargs["torch_device"] == "cuda": + return True return False diff --git a/optimas/generators/ax/service/base.py b/optimas/generators/ax/service/base.py index 61a861de..f7b39d49 100644 --- a/optimas/generators/ax/service/base.py +++ b/optimas/generators/ax/service/base.py @@ -10,13 +10,11 @@ ObjectiveProperties, FixedFeatures, ) -from ax.modelbridge.registry import Models -from ax.modelbridge.generation_strategy import ( - GenerationStep, - GenerationStrategy, -) -from ax.modelbridge.transition_criterion import MaxTrials, MinTrials -from ax import Arm +from ax.adapter.registry import Generators +from ax.generation_strategy.generation_node import GenerationStep +from ax.generation_strategy.generation_strategy import GenerationStrategy +from ax.generation_strategy.transition_criterion import MinTrials +from ax.core.arm import Arm from optimas.core import ( Trial, @@ -262,15 +260,19 @@ def _insert_unknown_trial(self, trial: Trial) -> None: # initialization trials, but only if they have not failed. if trial.completed and not self._enforce_n_init: generation_strategy = self._ax_client.generation_strategy - current_step = generation_strategy.current_step + current_node = generation_strategy.current_node # Reduce only if there are still Sobol trials left. - if current_step.model == Models.SOBOL: - for tc in current_step.transition_criteria: - # Looping over all criterial makes sure we reduce + is_sobol = any( + gs.generator_enum == Generators.SOBOL + for gs in current_node.generator_specs + ) + if is_sobol: + for tc in current_node.transition_criteria: + # Looping over all criteria makes sure we reduce # the transition thresholds due to `_n_init` # (i.e., max trials) and `min_trials_observed=1` ( # i.e., min trials). - if isinstance(tc, (MinTrials, MaxTrials)): + if isinstance(tc, MinTrials): tc.threshold -= 1 generation_strategy._maybe_transition_to_next_node() return ax_trial @@ -296,13 +298,11 @@ def _complete_trial(self, ax_trial_index: int, trial: Trial) -> None: def _create_ax_client(self) -> AxClient: """Create Ax client.""" bo_model_kwargs = { - "torch_dtype": torch.double, "torch_device": torch.device(self.torch_device), - "fit_out_of_design": self._fit_out_of_design, } ax_client = AxClient( generation_strategy=GenerationStrategy( - self._create_generation_steps(bo_model_kwargs) + nodes=self._create_generation_steps(bo_model_kwargs) ), verbose_logging=False, ) @@ -339,7 +339,7 @@ def _create_sobol_step(self) -> GenerationStep: # This also allows the generator to work well when # `sim_workers` > `n_init`. return GenerationStep( - model=Models.SOBOL, + generator=Generators.SOBOL, num_trials=self._n_init, min_trials_observed=1, enforce_num_trials=False, @@ -366,8 +366,8 @@ def _update_parameter(self, parameter): # Delete the fitted model from the generation strategy, otherwise # the parameter won't be updated. generation_strategy = self._ax_client.generation_strategy - if generation_strategy._model is not None: - del generation_strategy._curr.model_spec._fitted_model + if generation_strategy._curr.generator_spec._fitted_adapter is not None: + generation_strategy._curr.generator_spec._fitted_adapter = None parameters = self._create_ax_parameters() new_search_space = InstantiationBase.make_search_space(parameters, None) self._ax_client.experiment.search_space.update_parameter( diff --git a/optimas/generators/ax/service/multi_fidelity.py b/optimas/generators/ax/service/multi_fidelity.py index dbbefb01..2157be4c 100644 --- a/optimas/generators/ax/service/multi_fidelity.py +++ b/optimas/generators/ax/service/multi_fidelity.py @@ -5,13 +5,33 @@ from botorch.acquisition.knowledge_gradient import ( qMultiFidelityKnowledgeGradient, ) -from ax.utils.common.constants import Keys -from ax.modelbridge.generation_strategy import GenerationStep -from ax.modelbridge.registry import Models +from botorch.acquisition.input_constructors import ( + ACQF_INPUT_CONSTRUCTOR_REGISTRY, +) +from ax.generation_strategy.generation_node import GenerationStep +from ax.adapter.registry import Generators from .base import AxServiceGenerator from gest_api.vocs import VOCS +# Workaround for BoTorch bug: X_pending is not in the allowed variable +# kwargs for construct_inputs_qMFKG, but Ax always passes it. KG-based +# acquisition functions handle pending points via fantasization, so +# X_pending can be safely ignored. +_original_constructor = ACQF_INPUT_CONSTRUCTOR_REGISTRY[ + qMultiFidelityKnowledgeGradient +] + + +def _patched_constructor(*args, **kwargs): + kwargs.pop("X_pending", None) + return _original_constructor(*args, **kwargs) + + +ACQF_INPUT_CONSTRUCTOR_REGISTRY[qMultiFidelityKnowledgeGradient] = ( + _patched_constructor +) + class AxMultiFidelityGenerator(AxServiceGenerator): """Multifidelity Bayesian optimization using the Ax service API. @@ -97,8 +117,10 @@ def _create_generation_steps( self, bo_model_kwargs: Dict ) -> List[GenerationStep]: """Create generation steps for multifidelity optimization.""" - # Add acquisition function to model kwargs. - bo_model_kwargs["botorch_acqf_class"] = qMultiFidelityKnowledgeGradient + # Add acquisition function to generator kwargs. + bo_model_kwargs[ + "botorch_acqf_class" + ] = qMultiFidelityKnowledgeGradient # Make generation strategy. steps = [] @@ -109,22 +131,9 @@ def _create_generation_steps( # Continue indefinitely with GPKG. steps.append( GenerationStep( - model=Models.BOTORCH_MODULAR, + generator=Generators.BOTORCH_MODULAR, num_trials=-1, - model_kwargs={ - **bo_model_kwargs, - "acquisition_options": { - "X_pending": None, - "constraints": None, - }, - }, - model_gen_kwargs={ - "model_gen_options": { - Keys.ACQF_KWARGS: { - Keys.COST_INTERCEPT: self.fidel_cost_intercept - } - } - }, + generator_kwargs=bo_model_kwargs, ), ) diff --git a/optimas/generators/ax/service/single_fidelity.py b/optimas/generators/ax/service/single_fidelity.py index 2728cf89..3d75abe0 100644 --- a/optimas/generators/ax/service/single_fidelity.py +++ b/optimas/generators/ax/service/single_fidelity.py @@ -2,8 +2,8 @@ from typing import List, Optional, Dict -from ax.modelbridge.generation_strategy import GenerationStep -from ax.modelbridge.registry import Models +from ax.generation_strategy.generation_node import GenerationStep +from ax.adapter.registry import Generators from .base import AxServiceGenerator from gest_api.vocs import VOCS @@ -126,10 +126,10 @@ def _create_generation_steps( # Ax 0.5.0 detects if multi-objective. if self._fully_bayesian: # Use a SAAS model with qNEHVI/qNEI acquisition function - MODEL_CLASS = Models.SAASBO + GENERATOR_CLASS = Generators.SAASBO else: # Use a model with qNEHVI/qNEI acquisition function - MODEL_CLASS = Models.BOTORCH_MODULAR + GENERATOR_CLASS = Generators.BOTORCH_MODULAR # Make generation strategy. steps = [] @@ -140,9 +140,9 @@ def _create_generation_steps( # Continue indefinitely with BO. steps.append( GenerationStep( - model=MODEL_CLASS, + generator=GENERATOR_CLASS, num_trials=-1, - model_kwargs=bo_model_kwargs, + generator_kwargs=bo_model_kwargs, ) ) diff --git a/optimas/utils/ax/ax_model_manager.py b/optimas/utils/ax/ax_model_manager.py index ddd41302..d91a1c49 100644 --- a/optimas/utils/ax/ax_model_manager.py +++ b/optimas/utils/ax/ax_model_manager.py @@ -25,18 +25,16 @@ # Ax utilities for model building try: from ax.service.ax_client import AxClient - from ax.modelbridge.generation_strategy import ( - GenerationStep, - GenerationStrategy, - ) - from ax.modelbridge.registry import Models - from ax.modelbridge.torch import TorchModelBridge + from ax.generation_strategy.generation_node import GenerationStep + from ax.generation_strategy.generation_strategy import GenerationStrategy + from ax.adapter.registry import Generators + from ax.adapter.torch import TorchAdapter from ax.core.observation import ObservationFeatures from .other import ( convert_optimas_to_ax_parameters, convert_optimas_to_ax_objectives, ) - from ax import Arm + from ax.core.arm import Arm ax_installed = True except ImportError: @@ -47,7 +45,7 @@ if TYPE_CHECKING: from ax.service.ax_client import AxClient - from ax.modelbridge.torch import TorchModelBridge + from ax.adapter.torch import TorchAdapter class AxModelManager: @@ -105,11 +103,11 @@ def __init__( ) @property - def _model(self) -> TorchModelBridge: + def _model(self) -> TorchAdapter: """Get the model from the AxClient instance.""" # Make sure model is fitted. self.ax_client.fit_model() - return self.ax_client.generation_strategy.model + return self.ax_client.generation_strategy._curr.generator_spec.fitted_adapter def _build_ax_client_from_dataframe( self, @@ -145,8 +143,8 @@ def _build_ax_client_from_dataframe( # allow calling `model.predict`. Using MOO for multiobjective is # needed because otherwise calls to `get_pareto_optimal_parameters` # would fail. - model = Models.BOTORCH_MODULAR - gs = GenerationStrategy([GenerationStep(model=model, num_trials=-1)]) + generator = Generators.BOTORCH_MODULAR + gs = GenerationStrategy(nodes=[GenerationStep(generator=generator, num_trials=-1)]) ax_client = AxClient(generation_strategy=gs, verbose_logging=False) ax_client.create_experiment( parameters=axparameters, objectives=axobjectives diff --git a/pyproject.toml b/pyproject.toml index 08f02e36..a87c7a5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,11 +36,11 @@ test = [ 'flake8', 'pytest', 'pytest-mpi', - 'ax-platform >=0.5.0, <1.0.0', + 'ax-platform >=1.0.0', 'matplotlib', ] all = [ - 'ax-platform >=0.5.0, <1.0.0', + 'ax-platform >=1.0.0', 'matplotlib' ] diff --git a/tests/test_ax_generators.py b/tests/test_ax_generators.py index 57d566b5..b2f48e8f 100644 --- a/tests/test_ax_generators.py +++ b/tests/test_ax_generators.py @@ -249,7 +249,7 @@ def test_ax_single_fidelity_resume(): # Check that the sobol step has not been skipped. df = ax_client.get_trials_data_frame() assert len(df) == 1 - assert df["generation_method"].to_numpy()[0] == "Sobol" + assert "Sobol" in df["generation_node"].to_numpy()[0] else: # Check that the old evaluations were added @@ -258,7 +258,7 @@ def test_ax_single_fidelity_resume(): # Check that the sobol step has been skipped. df = ax_client.get_trials_data_frame() assert len(df) == 12 - assert df["generation_method"].to_numpy()[-1] == "BoTorch" + assert "BoTorch" in df["generation_node"].to_numpy()[-1] check_run_ax_service( ax_client, gen, exploration, n_failed_expected=2 @@ -799,11 +799,9 @@ def test_ax_service_init(): # are replaced by Manual trials. df = ax_client.get_trials_data_frame() for j in range(i): - assert df["generation_method"][j] is None + assert df["generation_node"][j] is None for k in range(i, n_init - 1): - assert df["generation_method"][k] == "Sobol" - - df["generation_method"][min(i, n_init)] == "BoTorch" + assert df["generation_node"][k] is not None and "Sobol" in df["generation_node"][k] # Try to load saved client from json. This used to fail when the SOBOL # step was skipped due to n_external > n_init. It is added here to prevent @@ -848,10 +846,9 @@ def test_ax_service_init(): # `n_external` Manual trials. df = ax_client.get_trials_data_frame() for j in range(n_external): - assert df["generation_method"][j] is None + assert df["generation_node"][j] is None for k in range(n_external, n_external + n_init): - assert df["generation_method"][k] == "Sobol" - df["generation_method"][n_external + n_init] == "BoTorch" + assert df["generation_node"][k] is not None and "Sobol" in df["generation_node"][k] if __name__ == "__main__": From 8c8e3551b6be516bc984307ddc34bfece0204248 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 18:17:45 -0500 Subject: [PATCH 02/10] Update CI --- .github/workflows/unix-noax.yml | 5 ++--- .github/workflows/unix-openmpi.yml | 5 ++--- .github/workflows/unix.yml | 5 ++--- pyproject.toml | 4 ++-- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/unix-noax.yml b/.github/workflows/unix-noax.yml index eca7991d..8364789a 100644 --- a/.github/workflows/unix-noax.yml +++ b/.github/workflows/unix-noax.yml @@ -29,11 +29,10 @@ jobs: name: Install dependencies run: | conda install -c conda-forge pytorch-cpu - conda install -c pytorch numpy pandas conda install -c conda-forge mpi4py mpich pip install .[test] - pip install git+https://github.com/campa-consortium/gest-api.git - pip install git+https://github.com/xopt-org/xopt.git + pip install gest-api + pip install xopt pip uninstall --yes ax-platform # Run without Ax - shell: bash -l {0} name: Run unit tests without Ax diff --git a/.github/workflows/unix-openmpi.yml b/.github/workflows/unix-openmpi.yml index fa4bf83d..a2675074 100644 --- a/.github/workflows/unix-openmpi.yml +++ b/.github/workflows/unix-openmpi.yml @@ -28,12 +28,11 @@ jobs: - shell: bash -l {0} name: Install dependencies run: | - conda install -c conda-forge "numpy<2.4" "pandas<3" conda install -c conda-forge pytorch-cpu conda install -c conda-forge mpi4py openmpi=5.* pip install .[test] - pip install git+https://github.com/campa-consortium/gest-api.git - pip install --upgrade-strategy=only-if-needed git+https://github.com/xopt-org/xopt.git + pip install gest-api + pip install xopt - shell: bash -l {0} name: Run unit tests with openMPI run: | diff --git a/.github/workflows/unix.yml b/.github/workflows/unix.yml index 4240c31c..f2abe0a4 100644 --- a/.github/workflows/unix.yml +++ b/.github/workflows/unix.yml @@ -28,12 +28,11 @@ jobs: - shell: bash -l {0} name: Install dependencies run: | - conda install -c conda-forge "numpy<2.4" "pandas<3" conda install -c conda-forge pytorch-cpu conda install -c conda-forge mpi4py mpich pip install .[test] - pip install git+https://github.com/campa-consortium/gest-api.git - pip install --upgrade-strategy=only-if-needed git+https://github.com/xopt-org/xopt.git + pip install gest-api + pip install xopt - shell: bash -l {0} name: Run unit tests with MPICH run: | diff --git a/pyproject.toml b/pyproject.toml index a87c7a5d..a03fe9ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,9 @@ classifiers = [ ] dependencies = [ 'libensemble >= 1.3.0', - 'numpy < 2.4', + 'numpy', 'jinja2', - 'pandas < 3', + 'pandas', 'mpi4py', 'pydantic >= 2.0', ] From 05c2eea87829662e362367ea13317660f1eaf693 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 18:18:35 -0500 Subject: [PATCH 03/10] Update libensemble dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a03fe9ce..855fc746 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ 'Programming Language :: Python :: 3.12', ] dependencies = [ - 'libensemble >= 1.3.0', + 'libensemble >= 1.6.0', 'numpy', 'jinja2', 'pandas', From 14a14d3abaed7428a3c408d260956b7d11525936 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:20:20 +0000 Subject: [PATCH 04/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- optimas/generators/ax/service/multi_fidelity.py | 4 +--- optimas/utils/ax/ax_model_manager.py | 8 ++++++-- tests/test_ax_generators.py | 10 ++++++++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/optimas/generators/ax/service/multi_fidelity.py b/optimas/generators/ax/service/multi_fidelity.py index 2157be4c..6c4e9ac8 100644 --- a/optimas/generators/ax/service/multi_fidelity.py +++ b/optimas/generators/ax/service/multi_fidelity.py @@ -118,9 +118,7 @@ def _create_generation_steps( ) -> List[GenerationStep]: """Create generation steps for multifidelity optimization.""" # Add acquisition function to generator kwargs. - bo_model_kwargs[ - "botorch_acqf_class" - ] = qMultiFidelityKnowledgeGradient + bo_model_kwargs["botorch_acqf_class"] = qMultiFidelityKnowledgeGradient # Make generation strategy. steps = [] diff --git a/optimas/utils/ax/ax_model_manager.py b/optimas/utils/ax/ax_model_manager.py index d91a1c49..a4645695 100644 --- a/optimas/utils/ax/ax_model_manager.py +++ b/optimas/utils/ax/ax_model_manager.py @@ -107,7 +107,9 @@ def _model(self) -> TorchAdapter: """Get the model from the AxClient instance.""" # Make sure model is fitted. self.ax_client.fit_model() - return self.ax_client.generation_strategy._curr.generator_spec.fitted_adapter + return ( + self.ax_client.generation_strategy._curr.generator_spec.fitted_adapter + ) def _build_ax_client_from_dataframe( self, @@ -144,7 +146,9 @@ def _build_ax_client_from_dataframe( # needed because otherwise calls to `get_pareto_optimal_parameters` # would fail. generator = Generators.BOTORCH_MODULAR - gs = GenerationStrategy(nodes=[GenerationStep(generator=generator, num_trials=-1)]) + gs = GenerationStrategy( + nodes=[GenerationStep(generator=generator, num_trials=-1)] + ) ax_client = AxClient(generation_strategy=gs, verbose_logging=False) ax_client.create_experiment( parameters=axparameters, objectives=axobjectives diff --git a/tests/test_ax_generators.py b/tests/test_ax_generators.py index b2f48e8f..3d578452 100644 --- a/tests/test_ax_generators.py +++ b/tests/test_ax_generators.py @@ -801,7 +801,10 @@ def test_ax_service_init(): for j in range(i): assert df["generation_node"][j] is None for k in range(i, n_init - 1): - assert df["generation_node"][k] is not None and "Sobol" in df["generation_node"][k] + assert ( + df["generation_node"][k] is not None + and "Sobol" in df["generation_node"][k] + ) # Try to load saved client from json. This used to fail when the SOBOL # step was skipped due to n_external > n_init. It is added here to prevent @@ -848,7 +851,10 @@ def test_ax_service_init(): for j in range(n_external): assert df["generation_node"][j] is None for k in range(n_external, n_external + n_init): - assert df["generation_node"][k] is not None and "Sobol" in df["generation_node"][k] + assert ( + df["generation_node"][k] is not None + and "Sobol" in df["generation_node"][k] + ) if __name__ == "__main__": From f364329fbd9a8e6007fceb33af615e74e3af5aca Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 18:36:43 -0500 Subject: [PATCH 05/10] Install torch with CPU-only index --- .github/workflows/unix-noax.yml | 2 +- .github/workflows/unix-openmpi.yml | 2 +- .github/workflows/unix.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unix-noax.yml b/.github/workflows/unix-noax.yml index 8364789a..7266c99c 100644 --- a/.github/workflows/unix-noax.yml +++ b/.github/workflows/unix-noax.yml @@ -28,8 +28,8 @@ jobs: - shell: bash -l {0} name: Install dependencies run: | - conda install -c conda-forge pytorch-cpu conda install -c conda-forge mpi4py mpich + pip install torch --index-url https://download.pytorch.org/whl/cpu pip install .[test] pip install gest-api pip install xopt diff --git a/.github/workflows/unix-openmpi.yml b/.github/workflows/unix-openmpi.yml index a2675074..07815338 100644 --- a/.github/workflows/unix-openmpi.yml +++ b/.github/workflows/unix-openmpi.yml @@ -28,8 +28,8 @@ jobs: - shell: bash -l {0} name: Install dependencies run: | - conda install -c conda-forge pytorch-cpu conda install -c conda-forge mpi4py openmpi=5.* + pip install torch --index-url https://download.pytorch.org/whl/cpu pip install .[test] pip install gest-api pip install xopt diff --git a/.github/workflows/unix.yml b/.github/workflows/unix.yml index f2abe0a4..1501faca 100644 --- a/.github/workflows/unix.yml +++ b/.github/workflows/unix.yml @@ -28,8 +28,8 @@ jobs: - shell: bash -l {0} name: Install dependencies run: | - conda install -c conda-forge pytorch-cpu conda install -c conda-forge mpi4py mpich + pip install torch --index-url https://download.pytorch.org/whl/cpu pip install .[test] pip install gest-api pip install xopt From 5637d28243c4f248e889b58e84d0ea0a4cef6e5e Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 18:55:45 -0500 Subject: [PATCH 06/10] Remove python 3.10 support --- .github/workflows/unix-noax.yml | 2 +- .github/workflows/unix-openmpi.yml | 2 +- .github/workflows/unix.yml | 2 +- pyproject.toml | 5 ++--- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unix-noax.yml b/.github/workflows/unix-noax.yml index 7266c99c..7903c0d0 100644 --- a/.github/workflows/unix-noax.yml +++ b/.github/workflows/unix-noax.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', 3.11, 3.12] + python-version: [3.11, 3.12] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/unix-openmpi.yml b/.github/workflows/unix-openmpi.yml index 07815338..053ac589 100644 --- a/.github/workflows/unix-openmpi.yml +++ b/.github/workflows/unix-openmpi.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', 3.11, 3.12] + python-version: [3.11, 3.12] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/unix.yml b/.github/workflows/unix.yml index 1501faca..da2bcc35 100644 --- a/.github/workflows/unix.yml +++ b/.github/workflows/unix.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', 3.11, 3.12] + python-version: [3.11, 3.12] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 855fc746..eae0540f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ {name = 'Optimas Developers', email = 'angel.ferran.pousa@desy.de'}, ] readme = 'README.md' -requires-python = '>=3.10' +requires-python = '>=3.11' keywords = ['optimization', 'scale', 'bayesian'] license = {text = 'BSD-3-Clause-LBNL'} classifiers = [ @@ -17,7 +17,6 @@ classifiers = [ 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ] @@ -58,7 +57,7 @@ include = [ [tool.black] line-length = 80 -target-version = ['py310', 'py311', 'py312'] +target-version = ['py311', 'py312'] [tool.pydocstyle] convention = "numpy" From 57d246e41cfff424c72d3c18e3419d71d159166d Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 20:21:14 -0500 Subject: [PATCH 07/10] Check for NaNs --- tests/test_ax_generators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_ax_generators.py b/tests/test_ax_generators.py index 3d578452..a24ec821 100644 --- a/tests/test_ax_generators.py +++ b/tests/test_ax_generators.py @@ -2,6 +2,7 @@ import threading import numpy as np +import pandas as pd from ax.service.ax_client import AxClient, ObjectiveProperties from ax.utils.measurement.synthetic_functions import hartmann6 @@ -799,7 +800,7 @@ def test_ax_service_init(): # are replaced by Manual trials. df = ax_client.get_trials_data_frame() for j in range(i): - assert df["generation_node"][j] is None + assert np.isnan(df["generation_node"][j]) for k in range(i, n_init - 1): assert ( df["generation_node"][k] is not None @@ -849,7 +850,7 @@ def test_ax_service_init(): # `n_external` Manual trials. df = ax_client.get_trials_data_frame() for j in range(n_external): - assert df["generation_node"][j] is None + assert np.isnan(df["generation_node"][j]) for k in range(n_external, n_external + n_init): assert ( df["generation_node"][k] is not None From 125f829fd29c111be630129106e91e238f7a90d9 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 21:04:19 -0500 Subject: [PATCH 08/10] Add back --- optimas/generators/ax/service/multi_fidelity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/optimas/generators/ax/service/multi_fidelity.py b/optimas/generators/ax/service/multi_fidelity.py index 6c4e9ac8..166d3f5f 100644 --- a/optimas/generators/ax/service/multi_fidelity.py +++ b/optimas/generators/ax/service/multi_fidelity.py @@ -127,6 +127,9 @@ def _create_generation_steps( steps.append(self._create_sobol_step()) # Continue indefinitely with GPKG. + bo_model_kwargs["botorch_acqf_options"] = { + "cost_intercept": self.fidel_cost_intercept, + } steps.append( GenerationStep( generator=Generators.BOTORCH_MODULAR, From e0ab73ec212cc5eb2d3ccc3772551d4e0d090ce4 Mon Sep 17 00:00:00 2001 From: shudson Date: Fri, 13 Mar 2026 22:36:05 -0500 Subject: [PATCH 09/10] Remove constraints test to MF --- tests/test_ax_generators.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_ax_generators.py b/tests/test_ax_generators.py index a24ec821..7175020f 100644 --- a/tests/test_ax_generators.py +++ b/tests/test_ax_generators.py @@ -2,7 +2,6 @@ import threading import numpy as np -import pandas as pd from ax.service.ax_client import AxClient, ObjectiveProperties from ax.utils.measurement.synthetic_functions import hartmann6 @@ -495,7 +494,11 @@ def test_ax_multi_fidelity(): vocs = VOCS( variables={"x0": [-50.0, 5.0], "x1": [-5.0, 15.0], "res": [1.0, 8.0]}, objectives={"f": "MAXIMIZE"}, - constraints={"p1": ["LESS_THAN", 30.0]}, + # Outcome constraints do not currently work with multi-fidelity + # when passed through to Ax. BoTorch's qMultiFidelityKnowledgeGradient + # is incompatible with the ModelListGP that Ax creates for + # multi-output models (objective + constraint). + # constraints={"p1": ["LESS_THAN", 30.0]}, ) gen = AxMultiFidelityGenerator(vocs=vocs) @@ -517,11 +520,6 @@ def test_ax_multi_fidelity(): # Run exploration. exploration.run() - # Check constraints. - ocs = gen._ax_client.experiment.optimization_config.outcome_constraints - assert len(ocs) == 1 - assert ocs[0].metric.name == "p1" - # Perform checks. check_run_ax_service(ax_client, gen, exploration, len(trials_to_fail)) @@ -694,7 +692,9 @@ def test_ax_multi_fidelity_with_history(): vocs = VOCS( variables={"x0": [-50.0, 5.0], "x1": [-5.0, 15.0], "res": [1.0, 8.0]}, objectives={"f": "MAXIMIZE"}, - constraints={"p1": ["LESS_THAN", 30.0]}, + # Outcome constraints do not currently work with multi-fidelity + # when passed through to Ax (see test_ax_multi_fidelity). + # constraints={"p1": ["LESS_THAN", 30.0]}, ) gen = AxMultiFidelityGenerator(vocs=vocs) @@ -800,7 +800,7 @@ def test_ax_service_init(): # are replaced by Manual trials. df = ax_client.get_trials_data_frame() for j in range(i): - assert np.isnan(df["generation_node"][j]) + assert not isinstance(df["generation_node"][j], str) for k in range(i, n_init - 1): assert ( df["generation_node"][k] is not None @@ -850,7 +850,7 @@ def test_ax_service_init(): # `n_external` Manual trials. df = ax_client.get_trials_data_frame() for j in range(n_external): - assert np.isnan(df["generation_node"][j]) + assert not isinstance(df["generation_node"][j], str) for k in range(n_external, n_external + n_init): assert ( df["generation_node"][k] is not None From b13796dd4db21c0af9771bc5a5c7ada72cead151 Mon Sep 17 00:00:00 2001 From: shudson Date: Mon, 16 Mar 2026 10:06:57 -0500 Subject: [PATCH 10/10] Update dependencies for docs --- doc/environment.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/environment.yaml b/doc/environment.yaml index 0815571d..cf45e731 100644 --- a/doc/environment.yaml +++ b/doc/environment.yaml @@ -5,13 +5,13 @@ dependencies: - pip - pip: - -e .. - - ax-platform >= 0.5.0, < 1.0.0 + - ax-platform >=1.0.0 - autodoc_pydantic >= 2.0.1 - ipykernel - matplotlib - nbsphinx - numpydoc - - git+https://github.com/campa-consortium/gest-api.git + - gest-api - pydata-sphinx-theme - sphinx-copybutton - sphinx-design