From 26b86e0d64bf3bc06bb90d7d7a66b44c73951f70 Mon Sep 17 00:00:00 2001 From: Elizabeth Santorella Date: Thu, 31 Oct 2024 06:23:32 -0700 Subject: [PATCH] Make optimizers raise an error when provided negative fixed features Summary: Context: See https://github.com/pytorch/botorch/issues/2602 This PR: * Adds a check for negative fixed_features keys to input validation for optimizers. This applies to all of the optimizers that take fixed_features. * Updates docstrings Differential Revision: D65272024 --- botorch/optim/optimize.py | 19 ++++++++++++----- test/optim/test_optimize.py | 42 ++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/botorch/optim/optimize.py b/botorch/optim/optimize.py index 390656161f..61e44d32b6 100644 --- a/botorch/optim/optimize.py +++ b/botorch/optim/optimize.py @@ -125,6 +125,10 @@ def __post_init__(self) -> None: "Must specify `raw_samples` when " "`batch_initial_conditions` is None`." ) + if self.fixed_features is not None and any( + (k < 0 for k in self.fixed_features) + ): + raise ValueError("All indices (keys) in `fixed_features` must be >= 0.") def get_ic_generator(self) -> TGenInitialConditions: if self.ic_generator is not None: @@ -467,7 +471,8 @@ def optimize_acqf( is set to 1, which will be done automatically if not specified in `options`. fixed_features: A map `{feature_index: value}` for features that - should be fixed to a particular value during generation. + should be fixed to a particular value during generation. All indices + should be non-negative. post_processing_func: A function that post-processes an optimization result appropriately (i.e., according to `round-trip` transformations). @@ -610,7 +615,8 @@ def optimize_acqf_cyclic( with each tuple encoding an inequality constraint of the form `\sum_i (X[indices[i]] * coefficients[i]) = rhs` fixed_features: A map `{feature_index: value}` for features that - should be fixed to a particular value during generation. + should be fixed to a particular value during generation. All indices + should be non-negative. post_processing_func: A function that post-processes an optimization result appropriately (i.e., according to `round-trip` transformations). @@ -758,11 +764,13 @@ def optimize_acqf_list( Using non-linear inequality constraints also requires that `batch_limit` is set to 1, which will be done automatically if not specified in `options`. - fixed_features: A map `{feature_index: value}` for features that - should be fixed to a particular value during generation. + fixed_features: A map `{feature_index: value}` for features that should + be fixed to a particular value during generation. All indices + (`feature_index`) should be non-negative. fixed_features_list: A list of maps `{feature_index: value}`. The i-th item represents the fixed_feature for the i-th optimization. If `fixed_features_list` is provided, `optimize_acqf_mixed` is invoked. + All indices (`feature_index`) should be non-negative. post_processing_func: A function that post-processes an optimization result appropriately (i.e., according to `round-trip` transformations). @@ -872,7 +880,8 @@ def optimize_acqf_mixed( raw_samples: Number of samples for initialization. This is required if `batch_initial_conditions` is not specified. fixed_features_list: A list of maps `{feature_index: value}`. The i-th - item represents the fixed_feature for the i-th optimization. + item represents the fixed_feature for the i-th optimization. All + indices (`feature_index`) should be non-negative. options: Options for candidate generation. inequality constraints: A list of tuples (indices, coefficients, rhs), with each tuple encoding an inequality constraint of the form diff --git a/test/optim/test_optimize.py b/test/optim/test_optimize.py index c501639c6d..c634ce4ef9 100644 --- a/test/optim/test_optimize.py +++ b/test/optim/test_optimize.py @@ -6,6 +6,7 @@ import itertools import warnings +from functools import partial from itertools import product from typing import Any from unittest import mock @@ -1119,6 +1120,45 @@ def __call__(self, x, f): self.assertEqual(f_np_wrapper.call_count, 2) +class TestAllOptimizers(BotorchTestCase): + def test_raises_with_negative_fixed_features(self) -> None: + cases = { + "optimize_acqf": partial( + optimize_acqf, + acq_function=MockAcquisitionFunction(), + fixed_features={-1: 0.0}, + q=1, + ), + "optimize_acqf_cyclic": partial( + optimize_acqf_cyclic, + acq_function=MockAcquisitionFunction(), + fixed_features={-1: 0.0}, + q=1, + ), + "optimize_acqf_mixed": partial( + optimize_acqf_mixed, + acq_function=MockAcquisitionFunction(), + fixed_features_list=[{-1: 0.0}], + q=1, + ), + "optimize_acqf_list": partial( + optimize_acqf_list, + acq_function_list=[MockAcquisitionFunction()], + fixed_features={-1: 0.0}, + ), + } + + for name, func in cases.items(): + with self.subTest(name), self.assertRaisesRegex( + ValueError, "must be >= 0." + ): + func( + bounds=torch.tensor([[0.0, 0.0], [1.0, 1.0]], device=self.device), + num_restarts=4, + raw_samples=16, + ) + + class TestOptimizeAcqfCyclic(BotorchTestCase): @mock.patch("botorch.optim.optimize._optimize_acqf") # noqa: C901 # TODO: make sure this runs without mock @@ -1171,7 +1211,7 @@ def test_optimize_acqf_cyclic(self, mock_optimize_acqf): "set_X_pending", wraps=mock_acq_function.set_X_pending, ) as mock_set_X_pending: - candidates, acq_value = optimize_acqf_cyclic( + candidates, _ = optimize_acqf_cyclic( acq_function=mock_acq_function, bounds=bounds, q=q,