Skip to content

Latest commit

 

History

History
256 lines (182 loc) · 7.85 KB

getting-started.rst

File metadata and controls

256 lines (182 loc) · 7.85 KB

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 ~mf2.multiFidelityFunction.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 represented1 .

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

Simple Usage

Accessing the functions

As an example, we'll use the ~mf2.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 function2 .

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 ~mf2.booth and 8D ~mf2.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 ~mf2.adjustable.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)