Input Features as Output Objectives

This notebook demonstrates how to put objectives on input features or a combination of input features. Possible usecases are favoring lower or higher amounts of an ingredient or to take into account a known (linear) cost function. In case of categorical inputs it can be used to penalize the optimizer for choosing specific categories.

Imports

import numpy as np

import bofire.strategies.api as strategies
import bofire.surrogates.api as surrogates
from bofire.benchmarks.api import Himmelblau
from bofire.data_models.features.api import CategoricalInput, ContinuousOutput
from bofire.data_models.objectives.api import (
    MaximizeObjective,
    MaximizeSigmoidObjective,
)
from bofire.data_models.strategies.api import MultiplicativeSoboStrategy
from bofire.data_models.surrogates.api import (
    BotorchSurrogates,
    CategoricalDeterministicSurrogate,
    LinearDeterministicSurrogate,
)

Setup an Example

We use Himmelblau as example with an additional objective on x_2 which pushes it to be larger 3 during the optimization. In addition, we introduce a categorical feature called x_cat which is mapped by an CategoricalDeterministicSurrogate to a continuous output called y_cat.

bench = Himmelblau()
experiments = bench.f(bench.domain.inputs.sample(10), return_complete=True)

domain = bench.domain

# setup extra feature `y_x2` that is the same as `x_2` and is taken into account in the optimization by a sigmoid objective
domain.outputs.features.append(
    ContinuousOutput(key="y_x2", objective=MaximizeSigmoidObjective(tp=3, steepness=10))
)
experiments["y_x2"] = experiments.x_2


# add extra categorical input feature and corresponding output feature
domain.inputs.features.append(CategoricalInput(key="x_cat", categories=["a", "b", "c"]))
domain.outputs.features.append(
    ContinuousOutput(key="y_cat", objective=MaximizeObjective())
)

# generate random values for the new categorical feature
experiments["x_cat"] = np.random.choice(["a", "b", "c"], size=experiments.shape[0])

The LinearDeterministicSurrogate can be used to model that y_x2 = x_2.

surrogate_data = LinearDeterministicSurrogate(
    inputs=domain.inputs.get_by_keys(["x_2"]),
    outputs=domain.outputs.get_by_keys(["y_x2"]),
    coefficients={"x_2": 1},
    intercept=0,
)
surrogate = surrogates.map(surrogate_data)
surrogate.predict(experiments[domain.inputs.get_keys()].copy())
/opt/hostedtoolcache/Python/3.12.12/x64/lib/python3.12/site-packages/bofire/surrogates/botorch.py:47: UserWarning:

The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at /pytorch/torch/csrc/utils/tensor_numpy.cpp:213.)
y_x2_pred y_x2_sd
0 -2.468130 0.0
1 -5.996417 0.0
2 5.923329 0.0
3 5.890397 0.0
4 2.774808 0.0
5 -4.732708 0.0
6 -3.315868 0.0
7 -0.059512 0.0
8 5.036469 0.0
9 -4.797046 0.0

The CategoricalDeterministicSurrogate can be used to map categories to specific continuous values.

categorical_surrogate_data = CategoricalDeterministicSurrogate(
    inputs=domain.inputs.get_by_keys(["x_cat"]),
    outputs=domain.outputs.get_by_keys(["y_cat"]),
    mapping={"a": 1, "b": 0.2, "c": 0.3},
)

surrogate = surrogates.map(categorical_surrogate_data)

surrogate.predict(experiments[domain.inputs.get_keys()].copy())

experiments["y_cat"] = surrogate.predict(experiments[domain.inputs.get_keys()].copy())[
    "y_cat_pred"
]

experiments
x_1 x_2 y valid_y y_x2 x_cat y_cat
0 4.612744 -2.468130 74.707459 1 -2.468130 b 0.2
1 -5.225205 -5.996417 669.419745 1 -5.996417 c 0.3
2 3.937051 5.923329 1134.118389 1 5.923329 a 1.0
3 0.320382 5.890397 810.030761 1 5.890397 b 0.2
4 -2.418140 2.774808 8.607420 1 2.774808 b 0.2
5 3.016126 -4.732708 383.131787 1 -4.732708 b 0.2
6 1.102378 -3.315868 197.609607 1 -3.315868 c 0.3
7 -4.192180 -0.059512 167.628998 1 -0.059512 a 1.0
8 4.331385 5.036469 678.944676 1 5.036469 b 0.2
9 -2.615902 -4.797046 259.622182 1 -4.797046 b 0.2

Next we setup a SoboStrategy using the custom surrogates for outputs y_x2 and y_cat and ask for a candidate. Note that the surrogate specs for output y is automatically generated and defaulted to be a SingleTaskGPSurrogate.

strategy_data = MultiplicativeSoboStrategy(
    domain=domain,
    surrogate_specs=BotorchSurrogates(
        surrogates=[surrogate_data, categorical_surrogate_data]
    ),
)
strategy = strategies.map(strategy_data)
strategy.tell(experiments)
strategy.ask(4)
x_1 x_2 x_cat y_pred y_cat_pred y_x2_pred y_sd y_cat_sd y_x2_sd y_des y_x2_des y_cat_des
0 -6.0 3.079681 b 76.913421 0.2 3.079681 91.516574 0.0 0.0 -76.913421 0.689292 0.2
1 -6.0 2.890514 b 40.713494 0.2 2.890514 88.085617 0.0 0.0 -40.713494 0.250704 0.2
2 6.0 2.960503 b 169.165859 0.2 2.960503 101.260353 0.0 0.0 -169.165859 0.402521 0.2
3 6.0 2.976304 c 268.778744 0.3 2.976304 130.640666 0.0 0.0 -268.778744 0.441037 0.3