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:

The name is simply a standardized format of the name as an attribute to help identify which function is being represented [1] .
Number of dimensions. This is the dimensionality (i.e. length) of the input vector X of which the objective is evaluated.
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.
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)
>>> 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))
>>> 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])

The object-style notation 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))
>>> 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(
    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)

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(
    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')


[1]This is as they’re instances of MultiFidelityFunction instead of separate classes.
[2]In fact, .ndim is defined as len(self.u_bound)