PyBADS Example 6: Periodic variables#
In this example, we will show how to run PyBADS on a target with periodic variables, such as angles.
This notebook is Part 6 of a series of notebooks in which we present various example usages for BADS with the PyBADS package. The code used in this example is available as a script here.
import numpy as np
from pybads import BADS
0. Periodic variables#
Some model parameters are periodic: an angle, a phase or a time of day describes the same state again after a whole period \(p\) (e.g., \(p = 2\pi\) for an angle in radians). The target function is then periodic along such a variable \(x_d\):
We tell PyBADS which variables are periodic with the option periodic_vars, a list of their indices (counted from 0, as any Python index). The hard bounds lower_bounds and upper_bounds of a periodic variable need to be finite, and are its period: \(p = \text{upper\_bounds}_d - \text{lower\_bounds}_d\). PyBADS takes the two bounds as the same point and wraps the variable around them, so that a step past one bound comes back in from the other.
1. Problem setup#
We optimize a function of \(D = 4\) variables, the sum of Rosenbrock’s banana function of the first two, as in Example 1, and of a cosine of each of the last two:
The function is periodic along \(x_3\), with period 4, and along \(x_4\), with period 2. Its global minimum is \(f^\star = 0\), at \(x_1 = x_2 = 1\), \(x_3 = \pm 2\) and \(x_4 = \pm 1\), where each cosine equals \(-1\).
We set the hard bounds of \(x_3\) and \(x_4\) to one period each, \([-2, 2]\) and \([-1, 1]\), and name them as periodic with periodic_vars = [2, 3]: they are the third and fourth variables, of indices 2 and 3 in Python. The bounds of a periodic variable only mark where its period is cut, not a range of extreme values, so we set its plausible bounds to its hard bounds as well. For \(x_1\) and \(x_2\), which are not periodic, we specify wide hard bounds and tighter plausible bounds, as in Example 1.
We start the optimization from \(\mathbf{x}_0 = (-3, -3, -1, -1)\), and fix the random seed of the run with random_seed, so that it is reproducible.
def rosenbrocks_fcn(x):
"""Rosenbrock's 'banana' function in any dimension."""
x_2d = np.atleast_2d(x)
return np.sum(
100 * (x_2d[:, 0:-1] ** 2 - x_2d[:, 1:]) ** 2
+ (x_2d[:, 0:-1] - 1) ** 2,
axis=1,
)
def periodic_fcn(x):
"""Rosenbrock's function of x_1, x_2, plus a cosine of x_3, of period 4,
and one of x_4, of period 2."""
x_2d = np.atleast_2d(x)
return (
rosenbrocks_fcn(x_2d[:, 0:2])
+ np.cos(np.pi * x_2d[:, 2] / 2)
+ np.cos(np.pi * x_2d[:, 3])
+ 2
)
lower_bounds = np.array([-10, -5, -2, -1])
upper_bounds = np.array([5, 10, 2, 1])
plausible_lower_bounds = np.array([-2, -2, -2, -1])
plausible_upper_bounds = np.array([2, 2, 2, 1])
x0 = np.array([-3, -3, -1, -1]) # Starting point
options = {
"periodic_vars": [2, 3], # The third and fourth variables are periodic
"random_seed": 0, # Makes the run reproducible
}
2. Run the optimization#
We run bads with the user-defined options. PyBADS lists the periodic variables when the BADS object is created.
bads = BADS(
periodic_fcn,
x0,
lower_bounds,
upper_bounds,
plausible_lower_bounds,
plausible_upper_bounds,
options=options,
)
optimize_result = bads.optimize()
Variables (index) defined with periodic boundaries: [2, 3]
Beginning optimization of a DETERMINISTIC objective function
Tip: Run PyBADS from at least 10 different starting points, ideally dozens, depending on your problem; with x0=None, each run draws its start at random in the plausible box. Keep the run with the lowest fval: a single run can stop in a local minimum, and if several runs reach nearly the same fval, you can be more confident in the solution.
https://acerbilab.github.io/pybads/faq.html#faq-how-do-i-run-pybads-from-several-starting-points
Iteration f-count f(x) MeshScale Method Actions
0 2 14417 1 Uncertainty test
0 10 73.9745 1 Initial mesh Initial points
1 16 72.1486 1 Successful poll Train
2 17 5.63746 1 Successful search (ES-wcm)
2 19 4.41123 1 Successful search (ES-wcm)
2 30 2.76902 1 Successful poll Train
3 32 1.22917 1 Successful search (ES-wcm)
3 46 1.22917 0.5 Refine grid Train
4 48 0.898619 0.5 Incremental search (ES-wcm)
4 51 0.893767 0.5 Incremental search (ES-wcm)
4 59 0.823291 0.25 Refine grid Train
5 61 0.525104 0.25 Successful search (ES-wcm)
5 62 0.0223908 0.25 Successful search (ES-wcm)
5 77 0.0223908 0.125 Refine grid Train
6 78 0.00775021 0.125 Incremental search (ES-wcm)
6 82 0.00704093 0.125 Incremental search (ES-ell)
6 90 0.00704093 0.0625 Refine grid
7 91 0.000348694 0.0625 Incremental search (ES-wcm)
7 93 0.000201998 0.0625 Incremental search (ES-ell)
7 103 0.000201998 0.03125 Refine grid Train
8 104 6.93178e-05 0.03125 Incremental search (ES-ell)
8 107 1.95059e-05 0.03125 Incremental search (ES-wcm)
8 116 1.95059e-05 0.015625 Refine grid Train
9 118 1.69339e-05 0.015625 Incremental search (ES-ell)
9 119 1.28228e-05 0.015625 Incremental search (ES-wcm)
9 121 7.37963e-06 0.015625 Incremental search (ES-wcm)
9 129 7.37963e-06 0.0078125 Refine grid Train
10 130 6.85736e-06 0.0078125 Incremental search (ES-ell)
10 132 4.74319e-06 0.0078125 Incremental search (ES-ell)
10 134 4.1912e-06 0.0078125 Incremental search (ES-wcm)
10 142 4.1912e-06 0.00195312 Refine grid Train
11 144 3.80632e-06 0.00195312 Incremental search (ES-ell)
11 155 3.80632e-06 0.000488281 Refine grid Train
12 156 3.65693e-06 0.000488281 Incremental search (ES-ell)
12 168 3.65693e-06 0.00012207 Refine grid Train
13 169 3.64768e-06 0.00012207 Incremental search (ES-ell)
Optimization terminated: change in the function value less than options['tol_fun'].
Function value at minimum: 3.6476772060645146e-06
3. Results and conclusions#
x_min = optimize_result["x"]
fval = optimize_result["fval"]
print(f"BADS minimum at: x_min = {x_min.flatten()}, fval = {fval:.4g}")
print(
f"total f-count: {optimize_result['func_count']}, time: {round(optimize_result['total_time'], 2)} s"
)
BADS minimum at: x_min = [ 1.00188157 1.00376138 -1.99977819 0.99990573], fval = 3.648e-06
total f-count: 169, time: 1.6 s
The true global minimum is at \(\mathbf{x}^\star = (1, 1, \pm 2, \pm 1)\), where \(f^\star = 0\).
PyBADS reports a periodic variable within its period, between lower_bounds and upper_bounds. Since the two bounds of a periodic variable are the same point, a minimum on them can come out near either end of the period: \(x_3\) at or just above \(-2\), or just below \(2\), and \(x_4\) at or just above \(-1\), or just below \(1\).
Remarks#
PyBADS takes the width of the hard bounds of a periodic variable as its period, so set them one period apart, such as
0and2 * np.pi, or-np.piandnp.pi, for an angle in radians. With bounds that are not a period apart, the target would jump where PyBADS wraps the variable around.For more information on periodic variables, see the PyBADS FAQ.