Skip to content

Introduction to domain and parameters¤

This section will give you information on how to set up your search space with the Domain class and the paramaters The Domain contains a dictionary of parameter instances for both the input_space and output_space that make up the feasible search space. This notebook demonstrates how to use the Domain class effectively, from initialization to advanced use cases.

The Domain class can be imported from the f3dasm.design module:

from f3dasm.design import Domain

A domain object can be created as follows


Creating a Domain Object¤

To start, we create an empty domain object:

domain = Domain()

Input Parameters¤

Now we will add some input parameters. You can use the add_parameter method to add an input parameter:

domain.add_parameter(name="x0")

Parameters can be of any type. f3dasm has built-in support for the following types:

  • floating point parameters
domain.add_float(name="x1", low=0.0, high=100.0)
domain.add_float(name="x2", low=0.0, high=4.0)
  • discrete integer parameters
domain.add_int(name="x3", low=2, high=4)
domain.add_int(name="x4", low=74, high=99)
  • categorical parameters
domain.add_category(name="x5", categories=["test1", "test2", "test3", "test4"])
domain.add_category(name="x6", categories=[0.9, 0.2, 0.1, -2])
  • constant parameters
domain.add_constant(name="x7", value=0.9)
  • array parameters
domain.add_array(name="x8", shape=(3,), low=0.0, high=1.0)

We can print the domain object to see the parameters that have been added:

print(domain)
Domain(
  Input Space: { x0: Parameter(type=object, to_disk=False), x1: ContinuousParameter(lower_bound=0.0, upper_bound=100.0, log=False), x2: ContinuousParameter(lower_bound=0.0, upper_bound=4.0, log=False), x3: DiscreteParameter(lower_bound=2, upper_bound=4, step=1), x4: DiscreteParameter(lower_bound=74, upper_bound=99, step=1), x5: CategoricalParameter(categories=['test1', 'test2', 'test3', 'test4']), x6: CategoricalParameter(categories=[0.9, 0.2, 0.1, -2]), x7: ConstantParameter(value=0.9), x8: ArrayParameter(shape=(3,), lower_bound=0.0, upper_bound=1.0) }
  Output Space: {  }
)

Output Parameters¤

Output parameters are the results of evaluating the input design with a data generation model. Output parameters can hold any type of data, e.g., a scalar value, a vector, a matrix, etc. Normally, you would not need to define output parameters, as they are created automatically when you store a variable to the ExperimentData object.

domain.add_output(name="y", to_disk=False)

Storing parameters on disk¤

As you will see in the next section, the ExperimentData object stores data associated with parameters. The data is stored in a tabular format, where each row corresponds to a single evaluation of the designspace. The columns of the table correspond to the input parameters and the output values.

Sometimes it is wise to store the data associated with a parameter separately outside this table: - when the data associated with a parameter is very large (e.g., large arrays or matrices), it allows you to lazy-load the data when needed - when the data should not or cannot be casted to a .csv file (e.g., a custom object)

You can choose to only store a reference in the ExperimentData object and store the data on disk. This can be done by setting the to_disk parameter to True when adding the parameter to the domain.

f3dasm supports storing and loading data for a few commonly used data types:

  • numpy arrays
  • pandas dataframes
  • xarray datasets and data arrays

For any other data types, you have to define custom functions to store and load data. You can attach these to a parameter via the store_function and load_function arguments of add_parameter / add_output.

A custom store_function receives the object together with a path without a file extension, and is responsible for picking the appropriate suffix, writing the object, and returning the full path including that extension. f3dasm uses the returned path verbatim to locate the object at load time, so the suffix on disk and in the return value must match. The matching load_function receives that same full path (with the extension) and returns the loaded object.

The following example demonstrates how to store and load a numpy array to and from disk. We will use a custom store and load function for this example, but these functions are not necessary for numpy arrays, as f3dasm provides built-in support for storing and loading numpy arrays:

from pathlib import Path

import numpy as np


def numpy_store(object: np.ndarray, path: str) -> str:
    """
    Store a numpy array.

    Parameters
    ----------
    object : np.ndarray
        The numpy array to store.
    path : str
        The path where the array will be stored.

    Returns
    -------
    str
        The path to the stored array.
    """
    _path = Path(path).with_suffix(".npy")
    np.save(file=_path, arr=object)
    return str(_path)


def numpy_load(path: str) -> np.ndarray:
    """
    Load a numpy array.

    Parameters
    ----------
    path : str
        The path to the array to load.

    Returns
    -------
    np.ndarray
        The loaded array.
    """
    _path = Path(path).with_suffix(".npy")
    return np.load(file=_path)

With these functions defined, we can add the parameter to the input of the domain:

domain.add_parameter(
    name="array_input",
    to_disk=True,
    store_function=numpy_store,
    load_function=numpy_load,
)

In the same fashion, we can add an output parameter to the domain:

domain.add_output(
    name="array_output",
    to_disk=True,
    store_function=numpy_store,
    load_function=numpy_load,
)

Filtering the Domain¤

The domain object can be filtered to only include certain types of parameters. This might be useful when you want to create a design of experiments with only continuous parameters, for example.

The attributes Domain.continuous, Domain.discrete, Domain.categorical, and Domain.constant can be used to filter the domain object.

print(f"Continuous domain: {domain.continuous}")
print(f"Discrete domain: {domain.discrete}")
print(f"Categorical domain: {domain.categorical}")
print(f"Constant domain: {domain.constant}")
print(f"Array domain: {domain.array}")
Continuous domain: Domain(
  Input Space: { x1: ContinuousParameter(lower_bound=0.0, upper_bound=100.0, log=False), x2: ContinuousParameter(lower_bound=0.0, upper_bound=4.0, log=False) }
  Output Space: {  }
)
Discrete domain: Domain(
  Input Space: { x3: DiscreteParameter(lower_bound=2, upper_bound=4, step=1), x4: DiscreteParameter(lower_bound=74, upper_bound=99, step=1) }
  Output Space: {  }
)
Categorical domain: Domain(
  Input Space: { x5: CategoricalParameter(categories=['test1', 'test2', 'test3', 'test4']), x6: CategoricalParameter(categories=[0.9, 0.2, 0.1, -2]) }
  Output Space: {  }
)
Constant domain: Domain(
  Input Space: { x7: ConstantParameter(value=0.9) }
  Output Space: {  }
)
Array domain: Domain(
  Input Space: { x8: ArrayParameter(shape=(3,), lower_bound=0.0, upper_bound=1.0) }
  Output Space: {  }
)

Storing the Domain object¤

The Domain object can be stored to disk using the store method. This method saves the domain object to a JSON file.

domain.store("my_domain.json")

The Domain object can be loaded from disk using the Domain.from_file method:

Domain.from_file("my_domain.json")
Domain(input_space={'x0': Parameter(to_disk=False), 'x1': ContinuousParameter(lower_bound=0.0, upper_bound=100.0, log=False), 'x2': ContinuousParameter(lower_bound=0.0, upper_bound=4.0, log=False), 'x3': DiscreteParameter(lower_bound=2, upper_bound=4, step=1), 'x4': DiscreteParameter(lower_bound=74, upper_bound=99, step=1), 'x5': CategoricalParameter(categories=['test1', 'test2', 'test3', 'test4']), 'x6': CategoricalParameter(categories=[0.9, 0.2, 0.1, -2]), 'x7': ConstantParameter(value=0.9), 'x8': ArrayParameter(shape=(3,), lower_bound=0.0, upper_bound=1.0), 'array_input': Parameter(to_disk=True)}, output_space={'y': Parameter(to_disk=False), 'array_output': Parameter(to_disk=True)})

Custom storing and loading functions will be encoded with pickle and converted to hexadecimal strings. This allows you to store and load custom functions without having to define them again.


Helper Function for Single-Objective, N-Dimensional Continuous Domains¤

We can easily create an \(n\)-dimensional continuous domain with the helper function make_nd_continuous_domain. We have to specify the boundaries (bounds) for each of the dimensions with a list of lists or a NumPy numpy.ndarray:

from f3dasm.design import make_nd_continuous_domain
bounds = [[-1.0, 1.0], [-1.0, 1.0]]
domain = make_nd_continuous_domain(bounds=bounds)

print(domain)
Domain(
  Input Space: { x0: ContinuousParameter(lower_bound=-1.0, upper_bound=1.0, log=False), x1: ContinuousParameter(lower_bound=-1.0, upper_bound=1.0, log=False) }
  Output Space: {  }
)

Next: Working with ExperimentData