# Targets

Targets play a crucial role as the connection between observables measured in an
experiment and the machine learning core behind BayBE.
In general, it is expected that you create one [`Target`](baybe.targets.base.Target)
object for each of your observables to inform BayBE about their existence.
The way BayBE treats these targets is then controlled via the
[`Objective`](../../userguide/objectives).

## NumericalTarget
```{admonition} Important
:class: important

The {class}`~baybe.targets.numerical.NumericalTarget` class has been redesigned from the
ground up in version [0.14.0](https://github.com/emdgroup/baybe/releases/),
providing a more concise and significantly **more expressive interface.**

For a temporary transition period, the class constructor offers full backward
compatibility with the previous interface, meaning that it can be called with either the
new or the legacy arguments. However, this comes at the cost of **reduced typing
support**, meaning that you won't get type hints (e.g. for autocompletion or static type
checks) for either of the two types of constructor calls.

For this reason, we offer two additional constructors available **for the duration of
the deprecation period** that offer full typing support:
{meth}`~baybe.targets.numerical.NumericalTarget.from_legacy_interface` and
{meth}`~baybe.targets.numerical.NumericalTarget.from_modern_interface`.
```

Use the {class}`~baybe.targets.numerical.NumericalTarget` class for optimizing
**real-valued** quantities.
Optimization with targets of this type follows two basic rules:
1. Targets are transformed as specified by their
   {attr}`~baybe.targets.numerical.NumericalTarget.transformation`, with no
   transformation defined being equivalent to the identity transformation.
2. Whenever an optimization direction is required (i.e., when the context is *not*
   [active learning](/userguide/active_learning)), the transformed targets are assumed
   to be **maximized** by default or **minimized** if explicitly specified via their
   {attr}`~baybe.targets.numerical.NumericalTarget.minimize` flag.

This results in a simple yet expressive interface:
```python
from baybe.targets import NumericalTarget
from baybe.transformations import LogarithmicTransformation

target = NumericalTarget(
    name="Yield",
    transformation=LogarithmicTransformation(),  # optional transformation
    minimize=False,  # this is the default
)
```

(targets-as-instruction)=
```{admonition} Targets are Optimization Instructions
:class: note

Notice how the target ingredients above declaratively specify the **different aspects**
of the underlying optimization problem:
* The {attr}`~baybe.targets.numerical.NumericalTarget.name` defines the signal
  **"source"**, i.e. the observable being measured.
* The {attr}`~baybe.targets.numerical.NumericalTarget.transformation` defines the
  **"what"**, i.e. which derivative of the signal is to be optimized.
* The {attr}`~baybe.targets.numerical.NumericalTarget.minimize` flag defines the
  **"how"**, i.e. the desired optimization direction.
```

While the second rule may seem restrictive at first, it does not limit the
expressiveness of the resulting models, thanks to the transformation step applied. In
fact, all types of optimization problems (e.g., minimization, matching/avoiding one or
[multiple set point values](../../examples/Transformations/laser_tuning), or pursuing
any other custom objective) are just maximization problems in disguise, hidden behind an
appropriate target transformation.


For example:
* **Minimization** can be achieved by negating the targets before maximizing the
  resulting numerical values. For more information, see [here](#minimization).
* **Matching** a set point value can be implemented by applying a transformation that
  computes the "proximity" to the set point in some way (e.g. in terms of the
  negative absolute difference to it). Similarly, avoiding the set point can be
  achieved by reversing the sign of the proximity measure (or activating the
  {attr}`~baybe.targets.numerical.NumericalTarget.minimize` flag in addition).
  For more information, see [here](#set-point-matching).
* In general, any (potentially nonlinear) **custom objective** can be expressed using a
  transformation that assigns higher values to more desirable outcomes and lower values
  to less desirable outcomes. Examples can be found 
  [here](../../examples/Transformations/Transformations).

Many cases – especially the first two described above – are so common that we offer
convenient ways to directly create the corresponding target objects for many
optimization workflows, eliminating the need to manually specify the necessary
{class}`~baybe.transformations.base.Transformation` object yourself:

### Minimization

Minimization of a target can be achieved by simply passing the `minimize=True` argument
to the constructor:
```python
from baybe.targets import NumericalTarget

t = NumericalTarget(
    name="Cost",
    minimize=True,  # cost is to be minimized
)
```

````{admonition} Minimization = Negated Maximization
:class: caution
Behind the scenes, minimization of targets is achieved by maximizing their negated
values: the {attr}`~baybe.targets.numerical.NumericalTarget.minimize` flag is used to
inform the corresponding {class}`~baybe.objectives.base.Objective` holding the
{class}`~baybe.targets.numerical.NumericalTarget` object to inject an appropriate
negating transformation **just before** passing the target values to the optimization
engine, allowing us to reuse the same maximization-based routines for all targets. The
details of this negation step depend on the objective type being used.

However, while numerically equivalent, there is a semantic difference between minimizing a
quantity and maximizing the negated signal derived from it. This difference is both
reflected by [the way targets are specified](#targets-as-instruction) as
well as by the resulting objects:
```python
import numpy as np
import pandas as pd
from pandas.testing import assert_frame_equal

from baybe.targets import NumericalTarget
from baybe.transformations import AffineTransformation

# Target 1: "Minimize" cost
t1 = NumericalTarget(name="Cost", minimize=True)

# Target 2: "Maximize" the quantity obtained from negating cost measurements
t2 = NumericalTarget(name="Cost", transformation=AffineTransformation(factor=-1))

# Although both targets yield the same objective values ...
s = pd.Series(np.linspace(0, 10), name="Cost")
df = s.to_frame()
assert_frame_equal(
    t1.to_objective().transform(df),
    t2.to_objective().transform(df),
)

# ... the targets themselves are not equal ...
assert t1 != t2

# ... and the transformed signals they specify differ!
assert not t1.transform(s).equals(t2.transform(s))
```
````

### Set Point Matching
For common matching transformations, we provide convenience constructors with the
`match_` prefix (see {class}`~baybe.targets.numerical.NumericalTarget` for all options).
Similar to [minimization](#minimization) targets, these constructors inject a
suitable transformation computing some form of "proximity" to the set point value.

While you can easily implement your own (potentially complex) matching logic using the
{class}`~baybe.transformations.basic.CustomTransformation` class, let us have a look at
how we can match a single set point using built-in constructors:

#### Absolute Transformation

![Transforms](../_static/targets/absolute.svg)

The potentially simplest way to match a set point value is by minimizing the absolute
distance to it, since it requires no configuration other than specifying the set point
value itself. The {meth}`~baybe.targets.numerical.NumericalTarget.match_absolute`
constructor allows you to do exactly that in a single line of code.

**Example**
```python
t_abs = NumericalTarget.match_absolute(name="Size", match_value=42)
```

```{admonition} Practical Considerations
:class: note
✅ Simple to use: no configuration required other than the set point value itself

❌ Cannot be used in situations where normalized targets are required
```

#### Triangular Transformation

![Transforms](../_static/targets/triangular.svg)

In some cases, we want to penalize [absolute distance](#absolute-transformation) to the set point but only
up to a certain threshold, above which any further deviation does not matter. For this purpose,
the {meth}`~baybe.targets.numerical.NumericalTarget.match_triangular` constructor can be used,
which allows us to specify these thresholds in various ways.

**Example**
```python
t1 = NumericalTarget.match_triangular(name="Size", match_value=42, width=10)
t2 = NumericalTarget.match_triangular(name="Size", match_value=42, cutoffs=(37, 47))
t3 = NumericalTarget.match_triangular(name="Size", match_value=42, margins=(5, 5))
assert t1 == t2 == t3
```

```{admonition} Practical Considerations
:class: note
✅ Normalized output: enables direct comparison with other normalized targets

✅ Possibility to directly specify an "acceptable range" around the set point value 

❌ Outside the triangular region, the gradient is zero, which can complicate
optimization if the thresholds are chosen too tight
```

#### Bell Transformation

![Transforms](../_static/targets/bell.svg)

Bell-transformed targets created via the
{meth}`~baybe.targets.numerical.NumericalTarget.match_bell` constructor can be
considered relaxed versions of their [triangular](#triangular-transformation)
counterparts. Unlike the latter, they have no strict cutoff points, resulting in a smooth
change in the output with non-zero gradient on the entire domain.

**Example**
```python
t_bell = NumericalTarget.match_bell(name="Size", match_value=42, sigma=5)
```

```{admonition} Practical Considerations
:class: note
✅ Normalized output: enables direct comparison with other normalized targets

✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Width of the bell is sometimes not intuitive to set
```

#### Power Transformation

![Transforms](../_static/targets/power.svg)

If you need more precise control over how strongly deviations from the set point are
penalized, you can use the {meth}`~baybe.targets.numerical.NumericalTarget.match_power`
constructor, which applies a power transformation to the [absolute distance](#absolute-transformation).
For the common case of squared penalties, we also provide a separate
{meth}`~baybe.targets.numerical.NumericalTarget.match_quadratic` constructor.

**Example**
```python
t_power = NumericalTarget.match_power(name="Size", match_value=42, exponent=2)
t_quad = NumericalTarget.match_quadratic(name="Size", match_value=42)
assert t_power == t_quad
```

```{admonition} Practical Considerations
:class: note
✅ Offers control over how strongly deviations from the set point are penalized

✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Cannot be used in situations where normalized targets are required
```

#### Advanced Matching Options
Instead of seeking to match a certain value, you might want to avoid that value 
altogether. The `match_*` constructors have the `mismatch_instead` argument for this 
purpose. If set to `True`, the resulting target will seek to avoid the specified 
`match_value`.

**Example**
```python
target = NumericalTarget.match_quadratic(
    name="Particle_Size",
    match_value=50,
    mismatch_instead=True,  # try to avoid particle size 50
)
```

The `match_*` constructors also have the `match_mode` argument. The default `"="`
corresponds to matching the exact `match_value`. But if you use `">="` or `"<="`, you
can indicate that the entire associated interval is a valid match. This enables to
express desired outcomes like:
- "The rate should be lower than 200 per minute, but it does not matter how much lower."
  ```python
  NumericalTarget.match_absolute("rate", 200, match_mode="<=")
  ```
- "The yield should be at least 80%, but it does not matter if it's even higher."
  ```python
  NumericalTarget.match_absolute("yield", 80, match_mode=">=")
  ```

Behind the scenes, BayBE transforms the targets in a way such that all 
values inside the region specified by the operator are considered equally good (or bad
if `mismatch_instead=True`). If the value of the target beyond the `match_value`
matters, it is rather a case for traditional maximization / minimization without match
considerations or applying [custom transformations](#custom-transformation).

**Example**
```python
target1 = NumericalTarget.match_absolute(
    name="yield",
    match_value=80,
    match_mode=">=",  # yield should be over 80. 95 is *not* considered better than 80
)
target2 = NumericalTarget(
    name="yield"  # yield should be as high as possible, 95 is considered better than 80
)
```

Below, we illustrate the overall transformations resulting from different combinations
of the arguments for the exemplary case of
{meth}`~baybe.targets.numerical.NumericalTarget.match_bell`:

![Advanced_Matching_Options](../_static/targets/matching.svg)

#### Custom Transformation
If none of the built-in constructors fit your needs because you need more fine-grained
control over the matching behavior (e.g. when there are multiple acceptable set points),
you always have the fallback option to create a
{class}`~baybe.transformations.basic.CustomTransformation` that implements the
corresponding logic and pass it to the regular
{class}`~baybe.targets.numerical.NumericalTarget` constructor.

### Target Normalization
Sometimes, it is necessary to normalize targets to the interval [0, 1] – especially when
multiple targets are present – in order to align them on a common scale. One situation
where this can be required is when combining the targets using a
{class}`~baybe.objectives.desirability.DesirabilityObjective`. For this purpose, we
provide convenience constructors with the `normalized_` prefix:

#### Ramp Transformation

![Transforms](../_static/targets/ramp.svg)

The {meth}`~baybe.targets.numerical.NumericalTarget.normalized_ramp` constructor offers
the simplest way to create a normalized target. It does so by linearly mapping the
target values to the range [0, 1] inside a specified interval and clamping the output
outside.

**Example**
```python
t = NumericalTarget.normalized_ramp(name="Target", cutoffs=(0, 1), descending=True)
```

```{admonition} Practical Considerations
:class: note
✅ Easy to interpret: output value changes linearly inside the specified range

❌ Outside the linear region, the gradient is zero, which can complicate
optimization if the thresholds are chosen too tight
```

#### Sigmoid Transformation

![Transforms](../_static/targets/sigmoid.svg)

The {meth}`~baybe.targets.numerical.NumericalTarget.normalized_sigmoid` constructor
can be considered a softened version of the [ramp transformation](#ramp-transformation).
Instead of using hard cutoffs, it smoothly interpolates the target values between 
0 and 1 using a sigmoid function. 

**Example**
```python
t = NumericalTarget.normalized_sigmoid(name="Target", anchors=[(-1, 0.1), (1, 0.9)])
```

```{admonition} Practical Considerations
:class: note
✅ Smooth gradient on the entire domain, which can be beneficial for optimization

❌ Requires more parameters to configure than the [ramp transformation](#ramp-transformation)
```


#### Normalizing Existing Targets
You can also create a normalized version of an existing target by calling its
{meth}`~baybe.targets.numerical.NumericalTarget.normalize` method, provided the target
already maps to a bounded domain. For brevity and demonstration purposes, we show an
example using [method chaining](method-chaining): 

```python
t = NumericalTarget(name="Target").power(2).clamp(max=1).normalize()
```

(method-chaining)=
### Creation From Existing Targets
Targets can also be quickly created from existing ones by calling certain transformation
methods on them (see {class}`~baybe.targets.numerical.NumericalTarget` for all options).

For example:
```python
t1 = NumericalTarget("Target")
t2 = t1 - 1  # subtract a constant
t3 = t2 / 5  # divide by a constant
t4 = t3.abs()  # compute absolute value
t5 = t4.power(3)  # compute the cube
t6 = t5.clamp(max=10)  # upper-bound to 10 (lower bound is 0 due to abs() call above)
t7 = t6.normalize()  # normalize to [0, 1]
```

## Limitations
```{important}
{class}`~baybe.targets.numerical.NumericalTarget` enables many use cases due to the
real-valued nature of most measurements. However, it can also be used to model
categorical targets if they are ordinal.

**For example:**
If your experimental outcome is a categorical ranking into "bad", "mediocre" and "good",
you could use a {class}`~baybe.targets.numerical.NumericalTarget`
by pre-mapping the categories to the values 1, 2 and 3, respectively.

If your target category is not ordinal, the transformation into a numerical target is
not straightforward, which is a current limitation of BayBE. We are looking into adding
more target variants in the future.
```