Skip to content

Commit

Permalink
Add FunParams class
Browse files Browse the repository at this point in the history
Introduce the FunParams class to handle UQ
test function parameters.
Updated class usage across the module
to ensure proper handling of parameters.
The test suite has been updated
to test latest changes.

This commit should resolve Issue #351.
  • Loading branch information
damar-wicaksono committed Oct 30, 2024
1 parent 8398664 commit 2396cc1
Show file tree
Hide file tree
Showing 40 changed files with 1,595 additions and 394 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The 20-dimensional polynomial test function from Alemazkoor
and Meidani (2018).
- The exponential distribution as a distribution of `UnivDist` instances.
- New class `FunParams` to organize function parameters.

### Changed

- The parameter in the Gramacy 1D sine function is now removed. Noise can
be added on the fly if needed.
- `evaluate()` abstract method is now must be implemented directly in the
concrete UQ test function; `eval_()` in the `UQTestFunABC` has been removed.

## [0.4.1] - 2023-10-27

Expand Down
2 changes: 2 additions & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ parts:
title: UnivDist
- file: api/input-spec
title: ProbInputSpec
- file: api/funparams
title: FunParams
- caption: Development
chapters:
- file: development/overview
Expand Down
9 changes: 9 additions & 0 deletions docs/api/funparams.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. _api_reference_function_parameters:

Function Parameters
===================

.. automodule:: uqtestfuns.core.parameters

.. autoclass:: uqtestfuns.core.parameters.FunParams
:members:
87 changes: 63 additions & 24 deletions docs/development/adding-test-function-implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,37 +138,39 @@ For an implementation of a test function, create a top module-level function
module):

```python
def evaluate(xx: np.ndarray, parameters: Any) -> np.ndarray:
def evaluate(xx: np.ndarray, a: float, b: float, c: float, r: float, s: float, t: float):
"""Evaluate the Branin function on a set of input values.
Parameters
----------
xx : np.ndarray
2-Dimensional input values given by an N-by-2 array where
N is the number of input values.
parameters : Any
The parameters of the test function (six numbers)
a : float
Parameter 'a' of the Branin function.
b : float
Parameter 'b' of the Branin function.
c : float
Parameter 'c' of the Branin function.
r : float
Parameter 'r' of the Branin function.
s : float
Parameter 's' of the Branin function.
t : float
Parameter 't' of the Branin function.
Returns
-------
np.ndarray
The output of the Branin function evaluated on the input values.
The output is a 1-dimensional array of length N.
The output is a 1-dimensional array of length N.
"""
params = parameters
yy = (
params[0]
* (
xx[:, 1]
- params[1] * xx[:, 0] ** 2
+ params[2] * xx[:, 0]
- params[3]
)
** 2
+ params[4] * (1 - params[5]) * np.cos(xx[:, 0])
+ params[4]
a * (xx[:, 1] - b * xx[:, 0]**2 + c * xx[:, 0] - r)**2
+ s * (1 - t) * np.cos(xx[:, 0])
+ s
)

return yy
```

Expand Down Expand Up @@ -231,14 +233,51 @@ Conventionally, we name this variable `AVAILABLE_PARAMETERS`:

```python
AVAILABLE_PARAMETERS = {
"Dixon1978": np.array(
[1.0, 5.1 / (2 * np.pi) ** 2, 5 / np.pi, 6, 10, 1 / 8 / np.pi]
)
"Dixon1978": {
"function_id": "Branin",
"description": "Parameter set for the Branin function from Dixon (1978)",
"declared_parameters": [
{
"keyword": "a",
"value": 1.0,
"type": float,
},
{
"keyword": "b",
"value": 5.1 / (2 * np.pi) ** 2,
"type": float,
},
{
"keyword": "c",
"value": 5 / np.pi,
"type": float,
},
{
"keyword": "r",
"value": 6.0,
"type": float,
},
{
"keyword": "s",
"value": 10.0,
"type": float,
},
{
"keyword": "t",
"value": 1 / 8 / np.pi,
"type": float,
},
],
},
}
```

The value of the parameters can be of any type, as long as it is consistent
with how the parameters are going to be consumed by the `evaluate()` function.
This is a nested dictionary, where each top key-value pair contains one set of
parameters from the literature.

The value of the parameters in the set can be of any type, as long as it is
consistent with how the parameters are going to be consumed
by the `evaluate()` function.

As before, if there are multiple parameter sets available in the literature,
additional key-value pair should be added here.
Expand All @@ -257,7 +296,7 @@ A concrete implementation of this base class requires the following:
The full definition of the class for the Branin test function is shown below.

```python
class Branin(UQTestFunFixDimABC):
class Branin(UQTestFunABC):
"""A concrete implementation of the Branin test function."""

_tags = ["optimization"] # Application tags
Expand Down
18 changes: 12 additions & 6 deletions docs/getting-started/tutorial-built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ yy_sample = my_testfun(xx_sample)
The histogram of the output values can be created as follows:

```{code-cell} ipython3
:tags: [hide-input]
import matplotlib.pyplot as plt
plt.hist(yy_sample, bins="auto", color="#8da0cb")
Expand Down Expand Up @@ -250,15 +252,17 @@ and stored in the `parameters` property:

```{code-cell} ipython3
my_testfun = uqtf.Ishigami()
my_testfun.parameters
print(my_testfun.parameters)
```

To assign different parameter values, override the property values of the instance.
To assign different parameter values, override the property values
of the instance by specifying the name in brackets just like a dictionary:
For example:

```{code-cell} ipython3
my_testfun.parameters = (7, 0.35)
my_testfun.parameters
my_testfun.parameters["a"] = 7.0
my_testfun.parameters["b"] = 0.35
```

Note that once set, the parameter values are kept constant
Expand All @@ -273,9 +277,11 @@ as illustrated in the figure below.
:tags: [remove-input]
xx_sample = my_testfun.prob_input.get_sample(10000)
my_testfun.parameters = (7, 0.05)
my_testfun.parameters["a"] = 7.0
my_testfun.parameters["b"] = 0.05
yy_param_1 = my_testfun(xx_sample)
my_testfun.parameters = (7, 0.35)
my_testfun.parameters["a"] = 7.0
my_testfun.parameters["b"] = 0.35
yy_param_2 = my_testfun(xx_sample)
plt.hist(yy_param_2, bins="auto", color="#fc8d62", label="parameter 2")
Expand Down
140 changes: 121 additions & 19 deletions docs/getting-started/tutorial-custom-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,32 @@ The typical values for the parameters are shown in the table below.
The first component of a test function is the evaluation function itself.
UQTestFuns requires for such a function to have at least the input array
as its first parameter.
When applicable, the parameters must be the second parameter of the function.
When applicable, the parameters must appear after the input array.
It's your choice whether to lump the parameters together or split them as
exemplified below.
The Branin evaluation function can be defined as a Python function as follows:

```{code-cell} ipython3
def evaluate_branin(xx: np.ndarray, params: np.ndarray):
def evaluate_branin(xx: np.ndarray, a: float, b: float, c: float, r: float, s: float, t: float):
"""Evaluate the Branin function on a set of input values.
Parameters
----------
xx : np.ndarray
2-Dimensional input values given by an N-by-2 array where
N is the number of input values.
params : np.ndarray
The parameters of the Branin function;
a 1-Dimensional array of length 5.
a : float
Parameter 'a' of the Branin function.
b : float
Parameter 'b' of the Branin function.
c : float
Parameter 'c' of the Branin function.
r : float
Parameter 'r' of the Branin function.
s : float
Parameter 's' of the Branin function.
t : float
Parameter 't' of the Branin function.
Returns
-------
Expand All @@ -115,22 +126,22 @@ def evaluate_branin(xx: np.ndarray, params: np.ndarray):
The output is a 1-dimensional array of length N.
"""
yy = (
params[0] * (xx[:, 1] - params[1] * xx[:, 0]**2 + params[2] * xx[:, 0] - params[3])**2
+ params[4] * (1 - params[5]) * np.cos(xx[:, 0])
+ params[4]
a * (xx[:, 1] - b * xx[:, 0]**2 + c * xx[:, 0] - r)**2
+ s * (1 - t) * np.cos(xx[:, 0])
+ s
)
return yy
```

## Input and parameters

The second and third components of a test function are
the (probabilistic) input specification and the parameters.
The second a test function is the (probabilistic) input specification.
The input specification of the Branin function consists of
two independent uniform random variables with different bounds.
In UQTestFuns, a probabilistic input model is represented by `ProbInput` class
and an instance of it can be defined as follows:
In UQTestFuns, a probabilistic input model is represented
by the the {ref}`ProbInput <api_reference_probabilistic_input>`
class and an instance of it can be defined as follows:

```{code-cell} ipython3
# Define a list of marginals
Expand All @@ -149,18 +160,52 @@ print it out to the terminal:
print(my_input)
```

Finally, the parameters of the Branin function defined above can be defined
as a NumPy array as follows:
## Parameters

The third and final component of a test function is the parameter set.
A test function may or may not have a parameter set; in the case of the Branin
function, the set contains six parameters $\{ a, b, c, r, s, t \}$.

A parameter set in UQTestFuns is represented
by the {ref}`FunParams <api_reference_function_parameters>` class.
As defined above, the test function requires six separate parameters.
An instance of `FunParams` can be created as follows:

```{code-cell} ipython3
my_params = np.array([1.0, 5.1 / (2 * np.pi)**2, 5 / np.pi, 6, 10, 1 / (8 * np.pi)])
my_params = uqtf.FunParams(
function_id="Branin",
parameter_id="custom",
description="Parameter set for the Branin function",
)
```

Each of the parameters can then be added to the set as follows:

```{code-cell} ipython3
my_params.add(keyword="a", value=1.0, type=float)
my_params.add(keyword="b", value=5.1 / (2 * np.pi)**2, type=float)
my_params.add(keyword="c", value=5 / np.pi, type=float)
my_params.add(keyword="r", value=6.0, type=float)
my_params.add(keyword="s", value=10.0, type=float)
my_params.add(keyword="t", value=1 / (8 * np.pi), type=float)
```

Notice that the values to the `keyword` parameter are consistent with the
names that appear in the function definition
The value of the parameters can practically be of any Python data type.
They only depend on how they are going to be consumed by the specified
evaluatuon function.

```{note}
The parameters can practically be of any Python data type.
They only depend on how they are going to be consumed
by the specified evaluation function.
In other words, you have full control on how to define the parameters.
The `type` parameter is optional; when specified the set value must be
consistent.
```

To verify if the instance has been created successfully,
print it out to the terminal:

```{code-cell} ipython3
print(my_params)
```

## Creating a test function
Expand Down Expand Up @@ -257,6 +302,63 @@ testing optimization algorithms.
Shown in the contour plot above are the locations of
the three global optima of the function.

You can change the value of the parameters on the fly
by accessing the parameter by its keyword. For example:

```{code-cell} ipython3
my_testfun.parameters["a"] = 3.5
my_testfun.parameters["t"] = 1 / (4 * np.pi)
```

which will alter the landscape.

```{code-cell} ipython3
:tags: [hide-input]
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
# --- Create a 2-D grid
xx_1 = np.linspace(
my_input.marginals[0].lower, my_input.marginals[0].upper, 2000
)
xx_2 = np.linspace(
my_input.marginals[1].lower, my_input.marginals[1].upper, 2000
)
mesh_2d = np.meshgrid(xx_1, xx_2)
xx_2d = np.array(mesh_2d).T.reshape(-1, 2)
yy_2d = my_testfun(xx_2d)
# --- Create the plots
fig = plt.figure(figsize=(11, 5))
# Surface
axs_1 = plt.subplot(121, projection='3d')
axs_1.plot_surface(
mesh_2d[0],
mesh_2d[1],
yy_2d.reshape(2000, 2000).T,
cmap="plasma",
linewidth=0,
antialiased=False,
alpha=0.5
)
axs_1.set_xlabel("$x_1$", fontsize=14)
axs_1.set_ylabel("$x_2$", fontsize=14)
axs_1.set_zlabel("$\mathcal{M}(x_1, x_2)$", fontsize=14)
# Contour
axs_2 = plt.subplot(122)
cf = axs_2.contourf(
mesh_2d[0], mesh_2d[1], yy_2d.reshape(2000, 2000).T, 20, cmap="plasma"
)
axs_2.set_xlabel("$x_1$", fontsize=14)
axs_2.set_ylabel("$x_2$", fontsize=14)
divider = make_axes_locatable(axs_2)
cax = divider.append_axes('right', size='5%', pad=0.05)
fig.colorbar(cf, cax=cax, orientation='vertical')
axs_2.axis('scaled');
```

## Concluding remarks

The test function created above only persists in the current Python session.
Expand Down
Loading

0 comments on commit 2396cc1

Please sign in to comment.