Skip to content

Commit fa3749c

Browse files
David Erikssonfacebook-github-bot
authored andcommitted
Add worst known feasible value to constrained problems (#3016)
Summary: Pull Request resolved: #3016 Add the worst known feasible value to the constrained BoTorch test functions. This is useful when evaluating the performance of different optimization methods as this value can be assigned to all infeasible trials. This has the desirable property that any feasible trial has better performance than an infeasible trial. Reviewed By: esantorella Differential Revision: D82384518 fbshipit-source-id: ed8b9b7aeec174da11d5bbbe40caa867ec265e87
1 parent 9a45872 commit fa3749c

File tree

4 files changed

+74
-13
lines changed

4 files changed

+74
-13
lines changed

botorch/test_functions/base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class ConstrainedBaseTestProblem(BaseTestProblem, ABC):
239239
num_constraints: int
240240
_check_grad_at_opt: bool = False
241241
constraint_noise_std: None | float | list[float] = None
242+
_worst_feasible_value: float | None = None
242243

243244
def evaluate_slack(self, X: Tensor, noise: bool = True) -> Tensor:
244245
r"""Evaluate the constraint slack on a set of points.
@@ -313,6 +314,26 @@ def _evaluate_slack_true(self, X: Tensor) -> Tensor:
313314
"""
314315
pass # pragma: no cover
315316

317+
@property
318+
def worst_feasible_value(self) -> float:
319+
r"""The worst feasible value of the objective function. This is useful when
320+
evaluating the performance of different optimization methods as this value
321+
can be assigned to all infeasible trials. This has the desirable property that
322+
any feasible trial has better performance than an infeasible trial.
323+
"""
324+
if isinstance(self, MultiObjectiveTestProblem):
325+
return 0.0 # Can return 0.0 for MOO since this is the smallest possible HV
326+
elif self._worst_feasible_value is not None:
327+
return (
328+
-self._worst_feasible_value
329+
if self.negate
330+
else self._worst_feasible_value
331+
)
332+
raise NotImplementedError(
333+
f"Problem {self.__class__.__name__} does not specify the "
334+
"worst feasible value."
335+
)
336+
316337

317338
class MultiObjectiveTestProblem(BaseTestProblem, ABC):
318339
r"""Base class for multi-objective test functions.

botorch/test_functions/synthetic.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ class ConstrainedGramacy(ConstrainedSyntheticTestFunction):
10641064
_bounds = [(0.0, 1.0), (0.0, 1.0)]
10651065
_optimizers = [(0.1954, 0.4044)]
10661066
_optimal_value = 0.5998 # approximate from [Gramacy2016]_
1067+
_worst_feasible_value = 1.732051 # Computed from 100 SLSQP restarts
10671068

10681069
def _evaluate_true(self, X: Tensor) -> Tensor:
10691070
"""
@@ -1122,6 +1123,10 @@ def __init__(
11221123
self.constraint_noise_std = self._validate_constraint_noise(
11231124
constraint_noise_std
11241125
)
1126+
if dim == 3:
1127+
self._worst_feasible_value = -0.0002735 # Computed from 100 SLSQP restarts
1128+
elif dim == 6:
1129+
self._worst_feasible_value = -0.0001346 # Computed from 100 SLSQP restarts
11251130

11261131
def _evaluate_slack_true(self, X: Tensor) -> Tensor:
11271132
return -X.norm(dim=-1, keepdim=True) + 1
@@ -1167,6 +1172,10 @@ def __init__(
11671172
self.constraint_noise_std = self._validate_constraint_noise(
11681173
constraint_noise_std
11691174
)
1175+
if dim == 3:
1176+
self._worst_feasible_value = -0.0002735 # Computed from 100 SLSQP restarts
1177+
elif dim == 6:
1178+
self._worst_feasible_value = -0.0001346 # Computed from 100 SLSQP restarts
11701179

11711180
def _evaluate_slack_true(self, X: Tensor) -> Tensor:
11721181
return -X.pow(2).sum(dim=-1, keepdim=True) + 1
@@ -1184,6 +1193,7 @@ class PressureVessel(ConstrainedSyntheticTestFunction):
11841193
num_constraints = 4
11851194
_bounds = [(0.0, 10.0), (0.0, 10.0), (10.0, 50.0), (150.0, 200.0)]
11861195
_optimal_value = 6059.946341 # from [CoelloCoello2002constraint]
1196+
_worst_feasible_value = 240526.7248 # Computed from 100 SLSQP restarts
11871197

11881198
def _evaluate_true(self, X: Tensor) -> Tensor:
11891199
x1, x2, x3, x4 = X.unbind(-1)
@@ -1224,6 +1234,7 @@ class WeldedBeamSO(ConstrainedSyntheticTestFunction):
12241234
num_constraints = 6
12251235
_bounds = [(0.125, 10.0), (0.1, 10.0), (0.1, 10.0), (0.1, 10.0)]
12261236
_optimal_value = 1.728226 # from [CoelloCoello2002constraint]
1237+
_worst_feasible_value = 19.01859 # Computed from 100 SLSQP restarts
12271238

12281239
def _evaluate_true(self, X: Tensor) -> Tensor:
12291240
x1, x2, x3, x4 = X.unbind(-1)
@@ -1279,6 +1290,7 @@ class TensionCompressionString(ConstrainedSyntheticTestFunction):
12791290
num_constraints = 4
12801291
_bounds = [(0.01, 1.0), (0.01, 1.0), (0.01, 20.0)]
12811292
_optimal_value = 0.012681 # from [CoelloCoello2002constraint]
1293+
_worst_feasible_value = 0.306081 # Computed from 100 SLSQP restarts
12821294

12831295
def _evaluate_true(self, X: Tensor) -> Tensor:
12841296
x1, x2, x3 = X.unbind(-1)
@@ -1320,6 +1332,7 @@ class SpeedReducer(ConstrainedSyntheticTestFunction):
13201332
(5.0, 5.5),
13211333
]
13221334
_optimal_value = 2996.3482 # from [Lemonge2010constrained]
1335+
_worst_feasible_value = 6117.3420 # Computed from 100 SLSQP restarts
13231336

13241337
def _evaluate_true(self, X: Tensor) -> Tensor:
13251338
x1, x2, x3, x4, x5, x6, x7 = X.unbind(-1)
@@ -1377,6 +1390,7 @@ class KeaneBumpFunction(ConstrainedSyntheticTestFunction):
13771390
40: -0.826624404,
13781391
50: -0.83078783,
13791392
}
1393+
_worst_feasible_value = 0.0 # Computed from 100 SLSQP restarts
13801394

13811395
def __init__(
13821396
self,

botorch/utils/testing.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
BaseTestProblem,
3434
ConstrainedBaseTestProblem,
3535
CorruptedTestProblem,
36+
MultiObjectiveTestProblem,
3637
)
3738
from botorch.test_functions.synthetic import Rosenbrock
3839
from botorch.utils.transforms import unnormalize
@@ -402,6 +403,25 @@ def test_evaluate_slack(self):
402403
else:
403404
self.assertTrue(is_equal.all().item())
404405

406+
def test_worst_feasible_value(self):
407+
"""Test that a function's worst_feasible_value is correctly computed,
408+
and defined if it should be.
409+
"""
410+
for dtype in (torch.float, torch.double):
411+
for f in self.functions:
412+
f.to(device=self.device, dtype=dtype)
413+
if f._worst_feasible_value is None:
414+
self.assertTrue(isinstance(f, MultiObjectiveTestProblem))
415+
self.assertGreaterEqual(f.worst_feasible_value, 0.0)
416+
else:
417+
worst_feas_val = f.worst_feasible_value
418+
worst_feas_val_exp = (
419+
-f._worst_feasible_value
420+
if f.negate
421+
else f._worst_feasible_value
422+
)
423+
self.assertEqual(worst_feas_val, worst_feas_val_exp)
424+
405425
@property
406426
@abstractmethod
407427
def functions(self) -> Sequence[BaseTestProblem]:

test/test_functions/test_synthetic.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def test_constraint_noise_length_validation(self):
144144
):
145145
DummyConstrainedSyntheticTestFunction(constraint_noise_std=[0.1, 0.2])
146146

147+
def test_worst_feasible_value_not_implemented(self):
148+
with self.assertRaises(NotImplementedError):
149+
DummyConstrainedSyntheticTestFunction().worst_feasible_value
150+
147151

148152
class TestAckley(
149153
BotorchTestCase, BaseTestProblemTestCaseMixIn, SyntheticTestFunctionTestCaseMixin
@@ -387,13 +391,14 @@ class TestConstrainedHartmann(
387391
SyntheticTestFunctionTestCaseMixin,
388392
ConstrainedTestProblemTestCaseMixin,
389393
):
390-
functions = [
391-
ConstrainedHartmann(dim=6, negate=True),
392-
ConstrainedHartmann(noise_std=0.1, dim=6, negate=True),
393-
ConstrainedHartmann(
394-
noise_std=0.1, constraint_noise_std=0.2, dim=6, negate=True
395-
),
396-
]
394+
for dim in [3, 6]:
395+
functions = [
396+
ConstrainedHartmann(dim=dim, negate=True),
397+
ConstrainedHartmann(noise_std=0.1, dim=dim, negate=True),
398+
ConstrainedHartmann(
399+
noise_std=0.1, constraint_noise_std=0.2, dim=dim, negate=True
400+
),
401+
]
397402

398403

399404
class TestConstrainedHartmannSmooth(
@@ -402,12 +407,13 @@ class TestConstrainedHartmannSmooth(
402407
SyntheticTestFunctionTestCaseMixin,
403408
ConstrainedTestProblemTestCaseMixin,
404409
):
405-
functions = [
406-
ConstrainedHartmannSmooth(dim=6, negate=True),
407-
ConstrainedHartmannSmooth(
408-
dim=6, noise_std=0.1, constraint_noise_std=0.2, negate=True
409-
),
410-
]
410+
for dim in [3, 6]:
411+
functions = [
412+
ConstrainedHartmannSmooth(dim=dim, negate=True),
413+
ConstrainedHartmannSmooth(
414+
dim=dim, noise_std=0.1, constraint_noise_std=0.2, negate=True
415+
),
416+
]
411417

412418

413419
class TestPressureVessel(

0 commit comments

Comments
 (0)