Skip to content

Commit cffd409

Browse files
authored
Moving Output Objective (#442)
* add possibility to have objectives modified during runtime * fix tests * fix bug * add some docstrings
1 parent 53782de commit cffd409

File tree

21 files changed

+371
-68
lines changed

21 files changed

+371
-68
lines changed

bofire/data_models/domain/features.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -739,15 +739,21 @@ def __call__(
739739
"""
740740
desis = pd.concat(
741741
[
742-
feat(experiments[f"{feat.key}_pred" if predictions else feat.key]) # type: ignore
742+
feat(
743+
experiments[f"{feat.key}_pred" if predictions else feat.key],
744+
experiments[f"{feat.key}_pred" if predictions else feat.key],
745+
) # type: ignore
743746
for feat in self.features
744747
if feat.objective is not None
745748
and not isinstance(feat, CategoricalOutput)
746749
]
747750
+ [
748751
(
749752
pd.Series(
750-
data=feat(experiments.filter(regex=f"{feat.key}(.*)_prob")),
753+
data=feat(
754+
experiments.filter(regex=f"{feat.key}(.*)_prob"), # type: ignore
755+
experiments.filter(regex=f"{feat.key}(.*)_prob"), # type: ignore
756+
),
751757
name=f"{feat.key}_pred",
752758
) # type: ignore
753759
if predictions

bofire/data_models/features/categorical.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,14 @@ def validate_objective_categories(self):
356356
raise ValueError("categories must match to objective categories")
357357
return self
358358

359-
def __call__(self, values: pd.Series) -> pd.Series:
359+
def __call__(self, values: pd.Series, values_adapt: pd.Series) -> pd.Series:
360360
if self.objective is None:
361361
return pd.Series(
362362
data=[np.nan for _ in range(len(values))],
363363
index=values.index,
364364
name=values.name,
365365
)
366-
return self.objective(values) # type: ignore
366+
return self.objective(values, values_adapt) # type: ignore
367367

368368
def validate_experimental(self, values: pd.Series) -> pd.Series:
369369
values = values.map(str)

bofire/data_models/features/continuous.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,14 @@ class ContinuousOutput(Output):
178178
default_factory=lambda: MaximizeObjective(w=1.0)
179179
)
180180

181-
def __call__(self, values: pd.Series) -> pd.Series:
181+
def __call__(self, values: pd.Series, values_adapt: pd.Series) -> pd.Series:
182182
if self.objective is None:
183183
return pd.Series(
184184
data=[np.nan for _ in range(len(values))],
185185
index=values.index,
186186
name=values.name,
187187
)
188-
return self.objective(values) # type: ignore
188+
return self.objective(values, values_adapt) # type: ignore
189189

190190
def validate_experimental(self, values: pd.Series) -> pd.Series:
191191
try:

bofire/data_models/objectives/api.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from typing import Union
22

3-
from bofire.data_models.objectives.categorical import (
4-
ConstrainedCategoricalObjective,
5-
)
3+
from bofire.data_models.objectives.categorical import ConstrainedCategoricalObjective
64
from bofire.data_models.objectives.identity import (
75
IdentityObjective,
86
MaximizeObjective,
@@ -12,6 +10,7 @@
1210
from bofire.data_models.objectives.sigmoid import (
1311
MaximizeSigmoidObjective,
1412
MinimizeSigmoidObjective,
13+
MovingMaximizeSigmoidObjective,
1514
SigmoidObjective,
1615
)
1716
from bofire.data_models.objectives.target import (
@@ -31,6 +30,7 @@
3130

3231
AnyConstraintObjective = Union[
3332
MaximizeSigmoidObjective,
33+
MovingMaximizeSigmoidObjective,
3434
MinimizeSigmoidObjective,
3535
TargetObjective,
3636
]
@@ -45,4 +45,5 @@
4545
TargetObjective,
4646
CloseToTargetObjective,
4747
ConstrainedCategoricalObjective,
48+
MovingMaximizeSigmoidObjective,
4849
]

bofire/data_models/objectives/categorical.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Literal, Union
1+
from typing import Dict, List, Literal, Optional, Union
22

33
import numpy as np
44
import pandas as pd
@@ -61,12 +61,16 @@ def from_dict_label(self) -> Dict:
6161
return dict(zip(d.values(), d.keys()))
6262

6363
def __call__(
64-
self, x: Union[pd.Series, np.ndarray]
64+
self,
65+
x: Union[pd.Series, np.ndarray],
66+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
6567
) -> Union[pd.Series, np.ndarray, float]:
6668
"""The call function returning a probabilistic reward for x.
6769
6870
Args:
6971
x (np.ndarray): A matrix of x values
72+
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
73+
update the objective parameters on the fly. Defaults to None.
7074
7175
Returns:
7276
np.ndarray: A reward calculated as inner product of probabilities and feasible objectives.

bofire/data_models/objectives/identity.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Tuple, Union
1+
from typing import Literal, Optional, Tuple, Union
22

33
import numpy as np
44
import pandas as pd
@@ -48,11 +48,17 @@ def validate_lower_upper(cls, bounds):
4848
)
4949
return bounds
5050

51-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
51+
def __call__(
52+
self,
53+
x: Union[pd.Series, np.ndarray],
54+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
55+
) -> Union[pd.Series, np.ndarray]:
5256
"""The call function returning a reward for passed x values
5357
5458
Args:
5559
x (np.ndarray): An array of x values
60+
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
61+
update the objective parameters on the fly. Defaults to None.
5662
5763
Returns:
5864
np.ndarray: The identity as reward, might be normalized to the passed lower and upper bounds
@@ -81,11 +87,17 @@ class MinimizeObjective(IdentityObjective):
8187

8288
type: Literal["MinimizeObjective"] = "MinimizeObjective"
8389

84-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
90+
def __call__(
91+
self,
92+
x: Union[pd.Series, np.ndarray],
93+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
94+
) -> Union[pd.Series, np.ndarray]:
8595
"""The call function returning a reward for passed x values
8696
8797
Args:
8898
x (np.ndarray): An array of x values
99+
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
100+
update the objective parameters on the fly. Defaults to None.
89101
90102
Returns:
91103
np.ndarray: The negative identity as reward, might be normalized to the passed lower and upper bounds

bofire/data_models/objectives/objective.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from abc import abstractmethod
2-
from typing import Union
2+
from typing import Optional, Union
33

44
import numpy as np
55
import pandas as pd
@@ -15,11 +15,17 @@ class Objective(BaseModel):
1515
type: str
1616

1717
@abstractmethod
18-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
18+
def __call__(
19+
self,
20+
x: Union[pd.Series, np.ndarray],
21+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
22+
) -> Union[pd.Series, np.ndarray]:
1923
"""Abstract method to define the call function for the class Objective
2024
2125
Args:
22-
x (np.ndarray): An array of x values
26+
x (np.ndarray): An array of x values for which the objective should be evaluated.
27+
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
28+
update the objective parameters on the fly. Defaults to None.
2329
2430
Returns:
2531
np.ndarray: The desirability of the passed x values

bofire/data_models/objectives/sigmoid.py

+53-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Union
1+
from typing import Literal, Optional, Union
22

33
import numpy as np
44
import pandas as pd
@@ -37,18 +37,63 @@ class MaximizeSigmoidObjective(SigmoidObjective):
3737

3838
type: Literal["MaximizeSigmoidObjective"] = "MaximizeSigmoidObjective"
3939

40-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
40+
def __call__(
41+
self,
42+
x: Union[pd.Series, np.ndarray],
43+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
44+
) -> Union[pd.Series, np.ndarray]:
4145
"""The call function returning a sigmoid shaped reward for passed x values.
4246
4347
Args:
4448
x (np.ndarray): An array of x values
49+
x_adapt (np.ndarray): An array of x values which are used to update the objective parameters on the fly.
4550
4651
Returns:
4752
np.ndarray: A reward calculated with a sigmoid function. The stepness and the tipping point can be modified via passed arguments.
4853
"""
4954
return 1 / (1 + np.exp(-1 * self.steepness * (x - self.tp)))
5055

5156

57+
class MovingMaximizeSigmoidObjective(SigmoidObjective):
58+
"""Class for a maximizing sigmoid objective with a moving turning point that depends on so far observed x values.
59+
60+
Attributes:
61+
w (float): float between zero and one for weighting the objective when used in a weighting based strategy.
62+
steepness (float): Steepness of the sigmoid function. Has to be greater than zero.
63+
tp (float): Relative turning point of the sigmoid function. The actual turning point is calculated by adding
64+
the maximum of the observed x values to the relative turning point.
65+
"""
66+
67+
type: Literal["MovingMaximizeSigmoidObjective"] = "MovingMaximizeSigmoidObjective"
68+
69+
def get_adjusted_tp(self, x: Union[pd.Series, np.ndarray]) -> float:
70+
"""Get the adjusted turning point for the sigmoid function.
71+
72+
Args:
73+
x (np.ndarray): An array of x values
74+
75+
Returns:
76+
float: The adjusted turning point for the sigmoid function.
77+
"""
78+
return x.max() + self.tp
79+
80+
def __call__(
81+
self, x: Union[pd.Series, np.ndarray], x_adapt: Union[pd.Series, np.ndarray]
82+
) -> Union[pd.Series, np.ndarray]:
83+
"""The call function returning a sigmoid shaped reward for passed x values.
84+
85+
Args:
86+
x (np.ndarray): An array of x values
87+
x_adapt (np.ndarray): An array of x values which are used to update the objective parameters on the fly.
88+
89+
Returns:
90+
np.ndarray: A reward calculated with a sigmoid function. The stepness and the tipping point can be modified via passed arguments.
91+
"""
92+
return 1 / (
93+
1 + np.exp(-1 * self.steepness * (x - self.get_adjusted_tp(x_adapt)))
94+
)
95+
96+
5297
class MinimizeSigmoidObjective(SigmoidObjective):
5398
"""Class for a minimizing a sigmoid objective
5499
@@ -60,11 +105,16 @@ class MinimizeSigmoidObjective(SigmoidObjective):
60105

61106
type: Literal["MinimizeSigmoidObjective"] = "MinimizeSigmoidObjective"
62107

63-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
108+
def __call__(
109+
self,
110+
x: Union[pd.Series, np.ndarray],
111+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
112+
) -> Union[pd.Series, np.ndarray]:
64113
"""The call function returning a sigmoid shaped reward for passed x values.
65114
66115
Args:
67116
x (np.ndarray): An array of x values
117+
x_adapt (np.ndarray): An array of x values which are used to update the objective parameters on the fly.
68118
69119
Returns:
70120
np.ndarray: A reward calculated with a sigmoid function. The stepness and the tipping point can be modified via passed arguments.

bofire/data_models/objectives/target.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Literal, Union
1+
from typing import Literal, Optional, Union
22

33
import numpy as np
44
import pandas as pd
@@ -27,7 +27,11 @@ class CloseToTargetObjective(Objective):
2727
target_value: float
2828
exponent: float
2929

30-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
30+
def __call__(
31+
self,
32+
x: Union[pd.Series, np.ndarray],
33+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
34+
) -> Union[pd.Series, np.ndarray]:
3135
return -1 * (np.abs(x - self.target_value) ** self.exponent)
3236

3337

@@ -48,11 +52,17 @@ class TargetObjective(Objective, ConstrainedObjective):
4852
tolerance: TGe0
4953
steepness: TGt0
5054

51-
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
55+
def __call__(
56+
self,
57+
x: Union[pd.Series, np.ndarray],
58+
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
59+
) -> Union[pd.Series, np.ndarray]:
5260
"""The call function returning a reward for passed x values.
5361
5462
Args:
5563
x (np.array): An array of x values
64+
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
65+
update the objective parameters on the fly. Defaults to None.
5666
5767
Returns:
5868
np.array: An array of reward values calculated by the product of two sigmoidal shaped functions resulting in a maximum at the target value.

bofire/plot/objective.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def plot_objective_plotly(
1313
lower: float,
1414
upper: float,
1515
values: Optional[pd.Series] = None,
16+
adapt_values: Optional[pd.Series] = None,
1617
layout_options: Optional[Dict] = None,
1718
):
1819
"""Plot the assigned objective.
@@ -22,6 +23,8 @@ def plot_objective_plotly(
2223
lower (float): lower bound for the plot
2324
upper (float): upper bound for the plot
2425
values (Optional[pd.Series], optional): If provided, scatter also the historical data in the plot. Defaults to None.
26+
adapt_values (Optional[pd.Series], optional): If provided, adapt the objective function to the passed values.
27+
Defaults to None.
2528
layout_options: (Dict, optional): Options that are passed to plotlys `update_layout`.
2629
"""
2730
if feature.objective is None:
@@ -30,14 +33,14 @@ def plot_objective_plotly(
3033
)
3134

3235
x = pd.Series(np.linspace(lower, upper, 5000))
33-
reward = feature.objective.__call__(x)
36+
reward = feature.objective.__call__(x, x_adapt=adapt_values) # type: ignore
3437

3538
fig1 = px.line(x=x, y=reward, title=feature.key)
3639

3740
if values is not None:
3841
fig2 = px.scatter(
3942
x=values,
40-
y=feature.objective.__call__(values),
43+
y=feature.objective.__call__(values, x_adapt=adapt_values), # type: ignore
4144
)
4245
fig = go.Figure(data=fig1.data + fig2.data) # type: ignore
4346
else:

bofire/strategies/predictives/mobo.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ def __init__(
4242

4343
def _get_acqfs(self, n) -> List[AcquisitionFunction]:
4444
assert self.is_fitted is True, "Model not trained."
45+
assert self.experiments is not None, "No experiments available."
4546

4647
X_train, X_pending = self.get_acqf_input_tensors()
4748

4849
# get etas and constraints
49-
constraints, etas = get_output_constraints(self.domain.outputs)
50+
constraints, etas = get_output_constraints(
51+
self.domain.outputs, experiments=self.experiments
52+
)
5053
if len(constraints) == 0:
5154
constraints, etas = None, 1e-3
5255
else:
@@ -87,10 +90,14 @@ def _get_acqfs(self, n) -> List[AcquisitionFunction]:
8790
return [acqf]
8891

8992
def _get_objective(self) -> GenericMCMultiOutputObjective:
90-
objective = get_multiobjective_objective(outputs=self.domain.outputs)
93+
assert self.experiments is not None
94+
objective = get_multiobjective_objective(
95+
outputs=self.domain.outputs, experiments=self.experiments
96+
)
9197
return GenericMCMultiOutputObjective(objective=objective)
9298

9399
def get_adjusted_refpoint(self) -> List[float]:
100+
assert self.experiments is not None, "No experiments available."
94101
if self.ref_point is None:
95102
df = self.domain.outputs.preprocess_experiments_all_valid_outputs(
96103
self.experiments

0 commit comments

Comments
 (0)