Skip to content

Commit

Permalink
add possibility to have objectives modified during runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
jduerholt committed Oct 2, 2024
1 parent 4b34e75 commit 4c2f7a8
Show file tree
Hide file tree
Showing 20 changed files with 332 additions and 114 deletions.
10 changes: 8 additions & 2 deletions bofire/data_models/domain/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,15 +739,21 @@ def __call__(
"""
desis = pd.concat(
[
feat(experiments[f"{feat.key}_pred" if predictions else feat.key]) # type: ignore
feat(
experiments[f"{feat.key}_pred" if predictions else feat.key],
experiments[f"{feat.key}_pred" if predictions else feat.key],
) # type: ignore
for feat in self.features
if feat.objective is not None
and not isinstance(feat, CategoricalOutput)
]
+ [
(
pd.Series(
data=feat(experiments.filter(regex=f"{feat.key}(.*)_prob")),
data=feat(
experiments.filter(regex=f"{feat.key}(.*)_prob"), # type: ignore
experiments.filter(regex=f"{feat.key}(.*)_prob"), # type: ignore
),
name=f"{feat.key}_pred",
) # type: ignore
if predictions
Expand Down
4 changes: 2 additions & 2 deletions bofire/data_models/features/categorical.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,14 +356,14 @@ def validate_objective_categories(self):
raise ValueError("categories must match to objective categories")
return self

def __call__(self, values: pd.Series) -> pd.Series:
def __call__(self, values: pd.Series, values_adapt: pd.Series) -> pd.Series:
if self.objective is None:
return pd.Series(
data=[np.nan for _ in range(len(values))],
index=values.index,
name=values.name,
)
return self.objective(values) # type: ignore
return self.objective(values, values_adapt) # type: ignore

def validate_experimental(self, values: pd.Series) -> pd.Series:
values = values.map(str)
Expand Down
4 changes: 2 additions & 2 deletions bofire/data_models/features/continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,14 @@ class ContinuousOutput(Output):
default_factory=lambda: MaximizeObjective(w=1.0)
)

def __call__(self, values: pd.Series) -> pd.Series:
def __call__(self, values: pd.Series, values_adapt: pd.Series) -> pd.Series:
if self.objective is None:
return pd.Series(
data=[np.nan for _ in range(len(values))],
index=values.index,
name=values.name,
)
return self.objective(values) # type: ignore
return self.objective(values, values_adapt) # type: ignore

def validate_experimental(self, values: pd.Series) -> pd.Series:
try:
Expand Down
7 changes: 4 additions & 3 deletions bofire/data_models/objectives/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import Union

from bofire.data_models.objectives.categorical import (
ConstrainedCategoricalObjective,
)
from bofire.data_models.objectives.categorical import ConstrainedCategoricalObjective
from bofire.data_models.objectives.identity import (
IdentityObjective,
MaximizeObjective,
Expand All @@ -12,6 +10,7 @@
from bofire.data_models.objectives.sigmoid import (
MaximizeSigmoidObjective,
MinimizeSigmoidObjective,
MovingMaximizeSigmoidObjective,
SigmoidObjective,
)
from bofire.data_models.objectives.target import (
Expand All @@ -31,6 +30,7 @@

AnyConstraintObjective = Union[
MaximizeSigmoidObjective,
MovingMaximizeSigmoidObjective,
MinimizeSigmoidObjective,
TargetObjective,
]
Expand All @@ -45,4 +45,5 @@
TargetObjective,
CloseToTargetObjective,
ConstrainedCategoricalObjective,
MovingMaximizeSigmoidObjective,
]
8 changes: 6 additions & 2 deletions bofire/data_models/objectives/categorical.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Literal, Union
from typing import Dict, List, Literal, Optional, Union

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

def __call__(
self, x: Union[pd.Series, np.ndarray]
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray, float]:
"""The call function returning a probabilistic reward for x.
Args:
x (np.ndarray): A matrix of x values
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
update the objective parameters on the fly. Defaults to None.
Returns:
np.ndarray: A reward calculated as inner product of probabilities and feasible objectives.
Expand Down
18 changes: 15 additions & 3 deletions bofire/data_models/objectives/identity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Tuple, Union
from typing import Literal, Optional, Tuple, Union

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -48,11 +48,17 @@ def validate_lower_upper(cls, bounds):
)
return bounds

def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
"""The call function returning a reward for passed x values
Args:
x (np.ndarray): An array of x values
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
update the objective parameters on the fly. Defaults to None.
Returns:
np.ndarray: The identity as reward, might be normalized to the passed lower and upper bounds
Expand Down Expand Up @@ -81,11 +87,17 @@ class MinimizeObjective(IdentityObjective):

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

def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
"""The call function returning a reward for passed x values
Args:
x (np.ndarray): An array of x values
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
update the objective parameters on the fly. Defaults to None.
Returns:
np.ndarray: The negative identity as reward, might be normalized to the passed lower and upper bounds
Expand Down
12 changes: 9 additions & 3 deletions bofire/data_models/objectives/objective.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from abc import abstractmethod
from typing import Union
from typing import Optional, Union

import numpy as np
import pandas as pd
Expand All @@ -15,11 +15,17 @@ class Objective(BaseModel):
type: str

@abstractmethod
def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
"""Abstract method to define the call function for the class Objective
Args:
x (np.ndarray): An array of x values
x (np.ndarray): An array of x values for which the objective should be evaluated.
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
update the objective parameters on the fly. Defaults to None.
Returns:
np.ndarray: The desirability of the passed x values
Expand Down
47 changes: 44 additions & 3 deletions bofire/data_models/objectives/sigmoid.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Union
from typing import Literal, Optional, Union

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -37,18 +37,54 @@ class MaximizeSigmoidObjective(SigmoidObjective):

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

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


class MovingMaximizeSigmoidObjective(SigmoidObjective):
type: Literal["MovingMaximizeSigmoidObjective"] = "MovingMaximizeSigmoidObjective"

def get_adjusted_tp(self, x: Union[pd.Series, np.ndarray]) -> float:
"""Get the adjusted turning point for the sigmoid function.
Args:
x (np.ndarray): An array of x values
Returns:
float: The adjusted turning point for the sigmoid function.
"""
return x.max() + self.tp

def __call__(
self, x: Union[pd.Series, np.ndarray], x_adapt: Union[pd.Series, np.ndarray]
) -> Union[pd.Series, np.ndarray]:
"""The call function returning a sigmoid shaped reward for passed x values.
Args:
x (np.ndarray): An array of x values
x_adapt (np.ndarray): An array of x values which are used to update the objective parameters on the fly.
Returns:
np.ndarray: A reward calculated with a sigmoid function. The stepness and the tipping point can be modified via passed arguments.
"""
return 1 / (
1 + np.exp(-1 * self.steepness * (x - self.get_adjusted_tp(x_adapt)))
)


class MinimizeSigmoidObjective(SigmoidObjective):
"""Class for a minimizing a sigmoid objective
Expand All @@ -60,11 +96,16 @@ class MinimizeSigmoidObjective(SigmoidObjective):

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

def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
"""The call function returning a sigmoid shaped reward for passed x values.
Args:
x (np.ndarray): An array of x values
x_adapt (np.ndarray): An array of x values which are used to update the objective parameters on the fly.
Returns:
np.ndarray: A reward calculated with a sigmoid function. The stepness and the tipping point can be modified via passed arguments.
Expand Down
16 changes: 13 additions & 3 deletions bofire/data_models/objectives/target.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Union
from typing import Literal, Optional, Union

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -27,7 +27,11 @@ class CloseToTargetObjective(Objective):
target_value: float
exponent: float

def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
return -1 * (np.abs(x - self.target_value) ** self.exponent)


Expand All @@ -48,11 +52,17 @@ class TargetObjective(Objective, ConstrainedObjective):
tolerance: TGe0
steepness: TGt0

def __call__(self, x: Union[pd.Series, np.ndarray]) -> Union[pd.Series, np.ndarray]:
def __call__(
self,
x: Union[pd.Series, np.ndarray],
x_adapt: Optional[Union[pd.Series, np.ndarray]] = None,
) -> Union[pd.Series, np.ndarray]:
"""The call function returning a reward for passed x values.
Args:
x (np.array): An array of x values
x_adapt (Optional[np.ndarray], optional): An array of x values which are used to
update the objective parameters on the fly. Defaults to None.
Returns:
np.array: An array of reward values calculated by the product of two sigmoidal shaped functions resulting in a maximum at the target value.
Expand Down
7 changes: 5 additions & 2 deletions bofire/plot/objective.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def plot_objective_plotly(
lower: float,
upper: float,
values: Optional[pd.Series] = None,
adapt_values: Optional[pd.Series] = None,
layout_options: Optional[Dict] = None,
):
"""Plot the assigned objective.
Expand All @@ -22,6 +23,8 @@ def plot_objective_plotly(
lower (float): lower bound for the plot
upper (float): upper bound for the plot
values (Optional[pd.Series], optional): If provided, scatter also the historical data in the plot. Defaults to None.
adapt_values (Optional[pd.Series], optional): If provided, adapt the objective function to the passed values.
Defaults to None.
layout_options: (Dict, optional): Options that are passed to plotlys `update_layout`.
"""
if feature.objective is None:
Expand All @@ -30,14 +33,14 @@ def plot_objective_plotly(
)

x = pd.Series(np.linspace(lower, upper, 5000))
reward = feature.objective.__call__(x)
reward = feature.objective.__call__(x, x_adapt=adapt_values) # type: ignore

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

if values is not None:
fig2 = px.scatter(
x=values,
y=feature.objective.__call__(values),
y=feature.objective.__call__(values, x_adapt=adapt_values), # type: ignore
)
fig = go.Figure(data=fig1.data + fig2.data) # type: ignore
else:
Expand Down
Loading

0 comments on commit 4c2f7a8

Please sign in to comment.