---
title: Input Features as Output Objectives
jupyter: python3
---


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

```{python}
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`.

```{python}
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`.

```{python}
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())
```

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

```{python}
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
```

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`.

```{python}
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)
```
