# Getting Started¶

This page contains some explained examples to help get you started with using
the `mf2`

package.

## The Basics: What’s in a MultiFidelityFunction?¶

This package serves as a collection of functions with multiple fidelity levels.
The number of levels is at least two, but differs by function. Each function is
encoded as a `MultiFidelityFunction`

with the following attributes:

`.name`

- The
*name*is simply a standardized format of the name as an attribute to help identify which function is being represented [1] . `.ndim`

- Number of dimensions. This is the dimensionality (i.e. length) of the input vector X of which the objective is evaluated.
`.fidelity_names`

- This is a list of the human-readable names given to each fidelity.
`.u_bound`

,`.l_bound`

- The upper and lower bounds of the search-space for the function.
`.functions`

- A list of the actual function references. You won’t typically need this list though, as will be explained next in Accessing the functions.

## Simple Usage¶

### Accessing the functions¶

As an example, we’ll use the `booth`

function. As we can see using
`.ndim`

and the bounds, it is two-dimensional:

```
>>> from mf2 import booth
>>> print(booth.ndim)
2
>>> print(booth.l_bound, booth.u_bound)
[-10. -10.] [10. 10.]
```

Most multi-fidelity functions in `mf2`

are *bi-fidelity* functions, but a
function can have any number of fidelities. A bi-fidelity function has two
fidelity levels, which are typically called `high`

and `low`

. You can
easily check the names of the fidelities by printing the `fidelity_names`

attribute of a function:

```
>>> print(len(booth.fidelity_names))
2
>>> print(booth.fidelity_names)
['high', 'low']
```

These are just the names of the fidelities. The functions they represent can be
accessed as an object-style *attribute*,

```
>>> print(booth.high)
<function booth_hf at 0x...>
```

as a dictionary-style *key*,

```
>>> print(booth['low'])
<function booth_lf at 0x...>
```

or with a list-style *index* (which just passes through to `.functions`

).

```
>>> print(booth[0])
<function booth_hf at 0x...>
>>> print(booth[0] is booth.functions[0])
True
```

The object-style notation `function.fidelity()`

is recommended for explicit
access, but the other notations are available for more dynamic usage. With the
list-style access, the *highest* fidelity is always at index *0*.

### Calling the functions¶

All functions in the `mf2`

package assume *row-vectors* as input. To evaluate
the function at a single point, it can be given as a simple Python list or 1D
numpy array. Multiple points can be passed to the function individually, or
combined into a 2D list/array. The output of the function will always be
returned as a 1D numpy array:

```
>>> X1 = [0.0, 0.0]
>>> print(booth.high(X1))
[74.]
>>> X2 = [
... [ 1.0, 1.0],
... [ 1.0, -1.0],
... [-1.0, 1.0],
... [-1.0, -1.0]
... ]
>>> print(booth.high(X2))
[ 20. 80. 72. 164.]
```

### Using the bounds¶

Each function also has a given upper and lower bound, stored as a 1D numpy array. They will be of the same length, and exactly as long as the dimensionality of the function [2] .

Below is an example function to create a uniform sample within the bounds:

```
import numpy as np
def sample_in_bounds(func, n_samples):
raw_sample = np.random.random((n_samples, func.ndim))
scale = func.u_bound - func.l_bound
sample = (raw_sample * scale) + func.l_bound
return sample
```

## Kinds of functions¶

### Fixed Functions¶

The majority of multi-fidelity functions in this package are ‘fixed’ functions. This means that everything about the function is fixed:

- dimensionality of the input
- number of fidelity levels
- relation between the different fidelity levels

Examples of these functions include the 2D `booth`

and 8D
`borehole`

functions.

### Dynamic Dimensionality Functions¶

Some functions are dynamic in the dimensionality of the input they accept. An
example of such a function is the `forrester`

function. The regular 1D
function is included as `mf2.forrester`

, but a custom n-dimensional version
can be obtained by calling the factory:

```
forrester_4d = mf2.Forrester(ndim=4)
```

This `forrester_4d`

is then a regular fixed function as seen before.

### Adjustable Functions¶

Other functions have a tunable parameter that can be used to adjust the correlation between the different high and low fidelity levels. For these too, you can simply call a factory that will return a version of that function with the parameter fixed to your specification:

```
paciorek_high_corr = mf2.adjustable.paciorek(a2=0.1)
```

The exact relationship between the input parameter and resulting correlation
can be found in the documentation of the specific functions. See for example
`paciorek`

.

## Adding Your Own¶

Each function is stored as a `MultiFidelityFunction`

-object, which contains
the dimensionality, intended upper/lower bounds, and of course all fidelity
levels. This class can also be used to define your own multi-fidelity function.

To do so, first define regular functions for each fidelity. Then create the
`MultiFidelityFunction`

object by passing a name, the upper and lower bounds,
and a tuple of the functions for the fidelities.

The following is an example for a 1-dimensional multi-fidelity function named
`my_mf_sphere`

with three fidelities:

```
import numpy as np
from mf2 import MultiFidelityFunction
def sphere_hf(x):
return x*x
def sphere_mf(x):
return x * np.sqrt(x) * np.sign(x)
def sphere_lf(x):
return np.abs(x)
my_mf_sphere = MultiFidelityFunction(
name='sphere',
u_bound=[1],
l_bound=[-1],
functions=(sphere_hf, sphere_mf, sphere_lf),
)
```

These functions can be accessed using list-style *indices*, but as no names
are given, the object-style *attributes* or dict-style *keys* won’t work:

```
>>> print(my_mf_sphere[0])
<function sphere_hf at 0x...>
>>> print(my_mf_sphere['medium'])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
...
IndexError: Invalid index 'medium'
>>> print(my_mf_sphere.low)
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
...
AttributeError: 'MultiFidelityFunction' object has no attribute 'low'
>>> print(my_mf_sphere.fidelity_names)
None
```

To enable access by attribute or key, a tuple containing a name for each fidelity
is required. Let’s extend the previous example by adding
`fidelity_names=('high', 'medium', 'low')`

:

```
my_named_mf_sphere = MultiFidelityFunction(
name='sphere',
u_bound=[1],
l_bound=[-1],
functions=(sphere_hf, sphere_mf, sphere_lf),
fidelity_names=('high', 'medium', 'low'),
)
```

Now we the attribute and key access will work:

```
>>> print(my_named_mf_sphere[0])
<function sphere_hf at 0x...>
>>> print(my_named_mf_sphere['medium'])
<function sphere_mf at 0x...>
>>> print(my_named_mf_sphere.low)
<function sphere_lf at 0x...>
>>> print(my_named_mf_sphere.fidelity_names)
('high', 'medium', 'low')
```

Footnotes

[1] | This is as they’re instances of MultiFidelityFunction instead of separate classes. |

[2] | In fact, `.ndim` is defined as `len(self.u_bound)` |