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\):

\[ f(x_1, \ldots, x_d + p, \ldots, x_D) = f(x_1, \ldots, x_d, \ldots, 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:

\[ f(\mathbf{x}) = \text{Rosenbrock}(x_1, x_2) + \cos\left(\frac{\pi x_3}{2}\right) + \cos\left(\pi x_4\right) + 2. \]

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 0 and 2 * np.pi, or -np.pi and np.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.