Introduction (Himmelblau’s function)¶
Let’s use blop
to minimize Himmelblau’s function, which has four global minima:
[1]:
from blop.utils import prepare_re_env
%run -i $prepare_re_env.__file__ --db-type=temp
[2]:
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt
from blop.utils import functions
x1 = x2 = np.linspace(-6, 6, 256)
X1, X2 = np.meshgrid(x1, x2)
F = functions.himmelblau(X1, X2)
plt.pcolormesh(x1, x2, F, norm=mpl.colors.LogNorm(vmin=1e-1, vmax=1e3), cmap="magma_r")
plt.colorbar()
plt.xlabel("x1")
plt.ylabel("x2")
[2]:
Text(0, 0.5, 'x2')
There are several things that our agent will need. The first ingredient is some degrees of freedom (these are always ophyd
devices) which the agent will move around to different inputs within each DOF’s bounds (the second ingredient). We define these here:
[3]:
from blop import DOF
dofs = [
DOF(name="x1", search_domain=(-6, 6)),
DOF(name="x2", search_domain=(-6, 6)),
]
We also need to give the agent something to do. We want our agent to look in the feedback for a variable called ‘himmelblau’, and try to minimize it.
[4]:
from blop import Objective
objectives = [Objective(name="himmelblau", description="Himmeblau's function", target="min")]
In our digestion function, we define our objective as a deterministic function of the inputs:
[5]:
def digestion(df):
for index, entry in df.iterrows():
df.loc[index, "himmelblau"] = functions.himmelblau(entry.x1, entry.x2)
return df
We then combine these ingredients into an agent, giving it an instance of databroker
so that it can see the output of the plans it runs.
[6]:
from blop import Agent
agent = Agent(
dofs=dofs,
objectives=objectives,
digestion=digestion,
db=db,
)
Without any data, we can’t make any inferences about what the function looks like, and so we can’t use any non-trivial acquisition functions. Let’s start by quasi-randomly sampling the parameter space, and plotting our model of the function:
[7]:
RE(agent.learn("quasi-random", n=36))
agent.plot_objectives()
Transient Scan ID: 1 Time: 2024-09-26 23:33:03
Persistent Unique Scan ID: '5eb15031-5eaa-4afb-86ca-2cfec06b5aba'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:03.5 | -0.283 | -0.764 |
| 2 | 23:33:03.5 | 0.498 | -2.435 |
| 3 | 23:33:03.5 | 1.379 | -3.121 |
| 4 | 23:33:03.5 | 2.004 | -5.272 |
| 5 | 23:33:03.5 | 2.533 | -4.093 |
| 6 | 23:33:03.5 | 3.899 | -4.328 |
| 7 | 23:33:03.5 | 4.730 | -5.190 |
| 8 | 23:33:03.5 | 5.399 | -1.492 |
| 9 | 23:33:03.5 | 3.233 | -2.119 |
| 10 | 23:33:03.5 | 1.866 | 0.218 |
| 11 | 23:33:03.5 | 2.881 | -0.051 |
| 12 | 23:33:03.5 | 4.947 | 1.238 |
| 13 | 23:33:03.5 | 4.424 | 1.900 |
| 14 | 23:33:03.5 | 5.918 | 5.018 |
| 15 | 23:33:03.5 | 3.990 | 5.943 |
| 16 | 23:33:03.5 | 2.255 | 5.522 |
| 17 | 23:33:03.5 | 3.449 | 4.215 |
| 18 | 23:33:03.5 | 0.751 | 2.545 |
| 19 | 23:33:03.5 | 0.366 | 3.336 |
| 20 | 23:33:03.5 | -0.534 | 4.715 |
| 21 | 23:33:03.5 | -3.246 | 5.734 |
| 22 | 23:33:03.5 | -2.845 | 4.052 |
| 23 | 23:33:03.5 | -2.038 | 1.832 |
| 24 | 23:33:03.5 | -1.345 | 1.030 |
| 25 | 23:33:03.5 | -2.129 | -0.260 |
| 26 | 23:33:03.5 | -2.338 | -1.625 |
| 27 | 23:33:03.5 | -1.785 | -3.928 |
| 28 | 23:33:03.5 | -0.832 | -4.554 |
| 29 | 23:33:03.5 | -4.481 | -5.996 |
| 30 | 23:33:03.5 | -4.890 | -3.611 |
| 31 | 23:33:03.5 | -5.978 | -2.832 |
| 32 | 23:33:03.5 | -3.390 | -0.682 |
| 33 | 23:33:03.5 | -3.888 | 0.526 |
| 34 | 23:33:03.5 | -3.668 | 2.066 |
| 35 | 23:33:03.5 | -4.741 | 2.711 |
| 36 | 23:33:03.5 | -5.387 | 3.408 |
+-----------+------------+------------+------------+
generator list_scan ['5eb15031'] (scan num: 1)
To decide which points to sample, the agent needs an acquisition function. The available acquisition function are here:
[8]:
agent.all_acqfs
[8]:
identifier | type | multitask_only | description | |
---|---|---|---|---|
name | ||||
expected_improvement | ei | analytic | False | The expected value of max(f(x) - \nu, 0), wher... |
expected_mean | em | analytic | False | The expected value at each input. |
noisy_expected_hypervolume_improvement | nehvi | analytic | True | It's like a big box. How big is the box? |
probability_of_improvement | pi | analytic | False | The probability that this input improves on th... |
upper_confidence_bound | ucb | analytic | False | The expected value, plus some multiple of the ... |
lower_bound_max_value_entropy | lbmve | monte_carlo | False | Max entropy search, basically |
monte_carlo_expected_improvement | qei | monte_carlo | False | The expected value of max(f(x) - \nu, 0), wher... |
monte_carlo_expected_mean | qem | monte_carlo | False | The expected value at each input. |
monte_carlo_noisy_expected_hypervolume_improvement | qnehvi | monte_carlo | True | It's like a big box. How big is the box? |
monte_carlo_probability_of_improvement | qpi | monte_carlo | False | The probability that this input improves on th... |
monte_carlo_upper_confidence_bound | qucb | monte_carlo | False | The expected value, plus some multiple of the ... |
grid | g | random | False | A grid scan over the parameters. |
quasi-random | qr | random | False | Sobol-sampled quasi-random points. |
random | r | random | False | Uniformly-sampled random points. |
Now we can start to learn intelligently. Using the shorthand acquisition functions shown above, we can see the output of a few different ones:
[9]:
agent.plot_acquisition(acqf="qei")
To decide where to go, the agent will find the inputs that maximize a given acquisition function:
[10]:
agent.ask("qei", n=1)
[10]:
{'points': {'x1': [2.805624936064067], 'x2': [2.5543350735758263]},
'acqf_name': 'monte_carlo_expected_improvement',
'acqf_obj': [71.24460830559357],
'acqf_kwargs': {'best_f': -7.620830666808241},
'duration_ms': 98.62576600005468,
'sequential': True,
'upsample': 1,
'read_only_values': array([], shape=(1, 0), dtype=float64)}
We can also ask the agent for multiple points to sample and it will jointly maximize the acquisition function over all sets of inputs, and find the most efficient route between them:
[11]:
res = agent.ask("qei", n=8, route=True)
agent.plot_acquisition(acqf="qei")
plt.scatter(res["points"]["x1"], res["points"]["x2"], marker="d", facecolor="w", edgecolor="k")
plt.plot(res["points"]["x1"], res["points"]["x2"], color="r")
[11]:
[<matplotlib.lines.Line2D at 0x7ff60c7ac6d0>]
All of this is automated inside the learn
method, which will find a point (or points) to sample, sample them, and retrain the model and its hyperparameters with the new data. To do 4 learning iterations of 8 points each, we can run
[12]:
RE(agent.learn("qei", n=4, iterations=8))
Transient Scan ID: 2 Time: 2024-09-26 23:33:13
Persistent Unique Scan ID: 'ee4f438f-2238-4869-b51e-8860022dc7d3'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:13.9 | -2.314 | 3.204 |
| 2 | 23:33:13.9 | 2.806 | 2.554 |
| 3 | 23:33:13.9 | 2.894 | 2.036 |
| 4 | 23:33:13.9 | -3.705 | -3.216 |
+-----------+------------+------------+------------+
generator list_scan ['ee4f438f'] (scan num: 2)
Transient Scan ID: 3 Time: 2024-09-26 23:33:16
Persistent Unique Scan ID: '0082259d-c37a-4e14-8020-6438aee7a5f9'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:16.5 | -3.863 | -3.678 |
| 2 | 23:33:16.5 | -2.949 | 3.157 |
| 3 | 23:33:16.5 | 3.250 | 1.218 |
| 4 | 23:33:16.5 | 3.655 | -2.739 |
+-----------+------------+------------+------------+
generator list_scan ['0082259d'] (scan num: 3)
Transient Scan ID: 4 Time: 2024-09-26 23:33:18
Persistent Unique Scan ID: 'dba17743-f126-4549-a86b-25d40bdae3f7'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:18.6 | 3.547 | -1.272 |
| 2 | 23:33:18.7 | 3.201 | 2.048 |
| 3 | 23:33:18.7 | 2.853 | 1.554 |
| 4 | 23:33:18.7 | -3.956 | -3.318 |
+-----------+------------+------------+------------+
generator list_scan ['dba17743'] (scan num: 4)
Transient Scan ID: 5 Time: 2024-09-26 23:33:21
Persistent Unique Scan ID: 'b28feff2-1bf7-413e-a039-a88f7c23438f'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:21.1 | -2.753 | 3.241 |
| 2 | 23:33:21.1 | 3.603 | -0.455 |
| 3 | 23:33:21.1 | 3.176 | -1.272 |
| 4 | 23:33:21.1 | 3.610 | -1.858 |
+-----------+------------+------------+------------+
generator list_scan ['b28feff2'] (scan num: 5)
Transient Scan ID: 6 Time: 2024-09-26 23:33:22
Persistent Unique Scan ID: '9bf6dc68-abdf-406b-8f99-12c713c15f25'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:22.7 | 3.522 | -1.753 |
| 2 | 23:33:22.7 | 3.079 | 1.819 |
| 3 | 23:33:22.7 | -2.785 | 2.982 |
| 4 | 23:33:22.7 | -2.935 | 3.326 |
+-----------+------------+------------+------------+
generator list_scan ['9bf6dc68'] (scan num: 6)
/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/botorch/optim/optimize.py:235: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):
[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]
Trying again with a new set of initial conditions.
candidate, acq_value = _optimize_acqf_batch(new_inputs)
Transient Scan ID: 7 Time: 2024-09-26 23:33:25
Persistent Unique Scan ID: '5ed38be6-992e-445a-ae98-f8503922eb20'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:25.9 | 2.077 | 2.795 |
| 2 | 23:33:25.9 | 3.563 | -1.966 |
| 3 | 23:33:25.9 | -3.659 | -2.909 |
| 4 | 23:33:25.9 | -3.770 | -3.347 |
+-----------+------------+------------+------------+
generator list_scan ['5ed38be6'] (scan num: 7)
/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/botorch/optim/optimize.py:235: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):
[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]
Trying again with a new set of initial conditions.
candidate, acq_value = _optimize_acqf_batch(new_inputs)
Transient Scan ID: 8 Time: 2024-09-26 23:33:29
Persistent Unique Scan ID: '6e18f8b5-7115-444e-bccd-a4b0358217f6'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:29.3 | -3.446 | -3.114 |
| 2 | 23:33:29.3 | 3.572 | -1.839 |
| 3 | 23:33:29.3 | 3.437 | 0.474 |
| 4 | 23:33:29.3 | 3.003 | 1.997 |
+-----------+------------+------------+------------+
generator list_scan ['6e18f8b5'] (scan num: 8)
/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/botorch/optim/optimize.py:235: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):
[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL_TERMINATION_IN_LNSRCH.')]
Trying again with a new set of initial conditions.
candidate, acq_value = _optimize_acqf_batch(new_inputs)
Transient Scan ID: 9 Time: 2024-09-26 23:33:30
Persistent Unique Scan ID: 'a1586bdf-31da-4d19-994d-6aae4a4ef0e1'
New stream: 'primary'
+-----------+------------+------------+------------+
| seq_num | time | x1 | x2 |
+-----------+------------+------------+------------+
| 1 | 23:33:30.3 | 3.003 | 1.992 |
| 2 | 23:33:30.3 | 2.987 | 2.089 |
| 3 | 23:33:30.3 | 3.707 | 2.099 |
| 4 | 23:33:30.3 | 3.571 | -1.840 |
+-----------+------------+------------+------------+
generator list_scan ['a1586bdf'] (scan num: 9)
[12]:
('ee4f438f-2238-4869-b51e-8860022dc7d3',
'0082259d-c37a-4e14-8020-6438aee7a5f9',
'dba17743-f126-4549-a86b-25d40bdae3f7',
'b28feff2-1bf7-413e-a039-a88f7c23438f',
'9bf6dc68-abdf-406b-8f99-12c713c15f25',
'5ed38be6-992e-445a-ae98-f8503922eb20',
'6e18f8b5-7115-444e-bccd-a4b0358217f6',
'a1586bdf-31da-4d19-994d-6aae4a4ef0e1')
Our agent has found all the global minima of Himmelblau’s function using Bayesian optimization, and we can ask it for the best point:
[13]:
agent.plot_objectives()
print(agent.best)
x1 3.003128
x2 1.996729
himmelblau 0.000339
time 2024-09-26 23:33:29.338146448
acqf monte_carlo_expected_improvement
Name: 63, dtype: object