13
13
import torch
14
14
from ax .benchmark .runners .base import BenchmarkRunner
15
15
from ax .core .types import TParamValue
16
- from ax .exceptions .core import UnsupportedError
17
- from botorch .test_functions .multi_objective import MultiObjectiveTestProblem
18
16
from botorch .test_functions .synthetic import BaseTestProblem , ConstrainedBaseTestProblem
19
17
from botorch .utils .transforms import normalize , unnormalize
20
18
from torch import Tensor
@@ -28,17 +26,15 @@ class ParamBasedTestProblem(ABC):
28
26
(Noise - if desired - is added by the runner.)
29
27
"""
30
28
31
- num_objectives : int
32
-
33
29
@abstractmethod
34
30
def evaluate_true (self , params : Mapping [str , TParamValue ]) -> Tensor :
35
- """Evaluate noiselessly."""
36
- .. .
31
+ """
32
+ Evaluate noiselessly .
37
33
38
- def evaluate_slack_true ( self , params : Mapping [ str , TParamValue ]) -> Tensor :
39
- raise NotImplementedError (
40
- f" { self . __class__ . __name__ } does not support constraints. "
41
- )
34
+ Returns :
35
+ 1d tensor of shape (num_outcomes,).
36
+ "" "
37
+ ...
42
38
43
39
44
40
@dataclass (kw_only = True )
@@ -57,24 +53,18 @@ class BoTorchTestProblem(ParamBasedTestProblem):
57
53
5 will correspond to 0.5 while evaluating the test problem.
58
54
If modified bounds are not provided, the test problem will be
59
55
evaluated using the raw parameter values.
60
- num_objectives: The number of objectives.
61
56
"""
62
57
63
58
botorch_problem : BaseTestProblem
64
59
modified_bounds : list [tuple [float , float ]] | None = None
65
- num_objectives : int = 1
66
60
67
61
def __post_init__ (self ) -> None :
68
- if isinstance (self .botorch_problem , MultiObjectiveTestProblem ):
69
- self .num_objectives = self .botorch_problem .num_objectives
70
- if self .botorch_problem .noise_std is not None :
71
- raise ValueError (
72
- "noise_std should be set on the runner, not the test problem."
73
- )
74
- if getattr (self .botorch_problem , "constraint_noise_std" , None ) is not None :
62
+ if (
63
+ self .botorch_problem .noise_std is not None
64
+ or getattr (self .botorch_problem , "constraint_noise_std" , None ) is not None
65
+ ):
75
66
raise ValueError (
76
- "constraint_noise_std should be set on the runner, not the test "
77
- "problem."
67
+ "noise should be set on the `BenchmarkRunner`, not the test function."
78
68
)
79
69
self .botorch_problem = self .botorch_problem .to (dtype = torch .double )
80
70
@@ -96,20 +86,11 @@ def tensorize_params(self, params: Mapping[str, int | float]) -> torch.Tensor:
96
86
# pyre-fixme [14]: inconsistent override
97
87
def evaluate_true (self , params : Mapping [str , float | int ]) -> torch .Tensor :
98
88
x = self .tensorize_params (params = params )
99
- return self .botorch_problem (x )
100
-
101
- # pyre-fixme [14]: inconsistent override
102
- def evaluate_slack_true (self , params : Mapping [str , float | int ]) -> torch .Tensor :
103
- if not isinstance (self .botorch_problem , ConstrainedBaseTestProblem ):
104
- raise UnsupportedError (
105
- "`evaluate_slack_true` is only supported when the BoTorch "
106
- "problem is a `ConstrainedBaseTestProblem`."
107
- )
108
- # todo: could return x so as to not recompute
109
- # or could do both methods together, track indices of outcomes,
110
- # and only negate the non-constraints
111
- x = self .tensorize_params (params = params )
112
- return self .botorch_problem .evaluate_slack_true (x )
89
+ objectives = self .botorch_problem (x ).view (- 1 )
90
+ if isinstance (self .botorch_problem , ConstrainedBaseTestProblem ):
91
+ constraints = self .botorch_problem .evaluate_slack_true (x ).view (- 1 )
92
+ return torch .cat ([objectives , constraints ], dim = - 1 )
93
+ return objectives
113
94
114
95
115
96
@dataclass (kw_only = True )
@@ -119,7 +100,7 @@ class ParamBasedTestProblemRunner(BenchmarkRunner):
119
100
120
101
Given a trial, the Runner will use its `test_problem` to evaluate the
121
102
problem noiselessly for each arm in the trial, and then add noise as
122
- specified by the `noise_std` and `constraint_noise_std` . It will return
103
+ specified by the `noise_std`. It will return
123
104
metadata including the outcome names and values of metrics.
124
105
125
106
Args:
@@ -132,64 +113,26 @@ class ParamBasedTestProblemRunner(BenchmarkRunner):
132
113
"""
133
114
134
115
test_problem : ParamBasedTestProblem
135
- noise_std : float | list [float ] | None = None
136
- constraint_noise_std : float | list [float ] | None = None
116
+ noise_std : float | list [float ] | dict [str , float ] = 0.0
137
117
138
- @property
139
- def _is_constrained (self ) -> bool :
140
- return isinstance (self .test_problem , BoTorchTestProblem ) and isinstance (
141
- self .test_problem .botorch_problem , ConstrainedBaseTestProblem
142
- )
143
-
144
- def get_noise_stds (self ) -> None | float | dict [str , float ]:
118
+ def get_noise_stds (self ) -> dict [str , float ]:
145
119
noise_std = self .noise_std
146
- noise_std_dict : dict [str , float ] = {}
147
- num_obj = self .test_problem .num_objectives
148
-
149
- # populate any noise_stds for constraints
150
- if self ._is_constrained :
151
- constraint_noise_std = self .constraint_noise_std
152
- if isinstance (constraint_noise_std , list ):
153
- for i , cns in enumerate (constraint_noise_std , start = num_obj ):
154
- if cns is not None :
155
- noise_std_dict [self .outcome_names [i ]] = cns
156
- elif constraint_noise_std is not None :
157
- noise_std_dict [self .outcome_names [num_obj ]] = constraint_noise_std
158
-
159
- # if none of the constraints are subject to noise, then we may return
160
- # a single float or None for the noise level
161
-
162
- if not noise_std_dict and not isinstance (noise_std , list ):
163
- return noise_std # either a float or None
164
-
165
- if isinstance (noise_std , list ):
166
- if not len (noise_std ) == num_obj :
167
- # this shouldn't be possible due to validation upon construction
168
- # of the multi-objective problem, but better safe than sorry
120
+ if isinstance (noise_std , float ):
121
+ return {name : noise_std for name in self .outcome_names }
122
+ elif isinstance (noise_std , dict ):
123
+ if not set (noise_std .keys ()) == set (self .outcome_names ):
169
124
raise ValueError (
170
- "Noise std must have length equal to number of objectives."
125
+ "Noise std must have keys equal to outcome names if given as "
126
+ "a dict."
171
127
)
172
- else :
173
- noise_std = [noise_std for _ in range (num_obj )]
174
-
175
- for i , noise_std_ in enumerate (noise_std ):
176
- if noise_std_ is not None :
177
- noise_std_dict [self .outcome_names [i ]] = noise_std_
178
-
179
- return noise_std_dict
128
+ return noise_std
129
+ # list of floats
130
+ return dict (zip (self .outcome_names , noise_std , strict = True ))
180
131
181
132
def get_Y_true (self , params : Mapping [str , TParamValue ]) -> Tensor :
182
133
"""Evaluates the test problem.
183
134
184
135
Returns:
185
- A `batch_shape x m`-dim tensor of ground truth (noiseless) evaluations.
136
+ An ` m`-dim tensor of ground truth (noiseless) evaluations.
186
137
"""
187
- Y_true = self .test_problem .evaluate_true (params ).view (- 1 )
188
- if self ._is_constrained :
189
- # Convention: Concatenate objective and black box constraints. `view()`
190
- # makes the inputs 1d, so the resulting `Y_true` are also 1d.
191
- Y_true = torch .cat (
192
- [Y_true , self .test_problem .evaluate_slack_true (params ).view (- 1 )],
193
- dim = - 1 ,
194
- )
195
- return Y_true
138
+ return torch .atleast_1d (self .test_problem .evaluate_true (params = params ))
0 commit comments