{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Advanced tour of the Bayesian Optimization package"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bayes_opt import BayesianOptimization"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Suggest-Evaluate-Register Paradigm\n",
    "\n",
    "Internally the `maximize` method is simply a wrapper around the methods `suggest`, `probe`, and `register`. If you need more control over your optimization loops the Suggest-Evaluate-Register paradigm should give you that extra flexibility.\n",
    "\n",
    "For an example of running the `BayesianOptimization` in a distributed fashion (where the function being optimized is evaluated concurrently in different cores/machines/servers), checkout the `async_optimization.py` script in the examples folder."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's start by defining our function, bounds, and instantiating an optimization object.\n",
    "def black_box_function(x, y):\n",
    "    return -x ** 2 - (y - 1) ** 2 + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One extra ingredient we will need is an `AcquisitionFunction`, such as `acquisition.UpperConfidenceBound`. In case it is not clear why, take a look at the literature to understand better how this method works."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bayes_opt import acquisition\n",
    "\n",
    "acq = acquisition.UpperConfidenceBound(kappa=2.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the evaluation of the blackbox function will NOT be carried out by the optimizer object. We are simulating a situation where this function could be being executed in a different machine, maybe it is written in another language, or it could even be the result of a chemistry experiment. Whatever the case may be, you can take charge of it and as long as you don't invoke the `probe` or `maximize` methods directly, the optimizer object will ignore the blackbox function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=None,\n",
    "    acquisition_function=acq,\n",
    "    pbounds={'x': (-2, 2), 'y': (-3, 3)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `suggest` method of our optimizer can be called at any time. What you get back is a suggestion for the next parameter combination the optimizer wants to probe.\n",
    "\n",
    "Notice that while the optimizer hasn't observed any points, the suggestions will be random. However, they will stop being random and improve in quality the more points are observed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Next point to probe is: {'x': np.float64(-0.331911981189704), 'y': np.float64(1.3219469606529486)}\n"
     ]
    }
   ],
   "source": [
    "next_point_to_probe = optimizer.suggest()\n",
    "print(\"Next point to probe is:\", next_point_to_probe)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You are now free to evaluate your function at the suggested point however/whenever you like."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Found the target value to be: 0.7861845912690544\n"
     ]
    }
   ],
   "source": [
    "target = black_box_function(**next_point_to_probe)\n",
    "print(\"Found the target value to be:\", target)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Last thing left to do is to tell the optimizer what target value was observed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "| \u001b[39m2        \u001b[39m | \u001b[39m0.7862   \u001b[39m | \u001b[39m-0.331911\u001b[39m | \u001b[39m1.3219469\u001b[39m |\n"
     ]
    }
   ],
   "source": [
    "optimizer.register(\n",
    "    params=next_point_to_probe,\n",
    "    target=target,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1.1 The maximize loop\n",
    "\n",
    "And that's it. By repeating the steps above you recreate the internals of the `maximize` method. This should give you all the flexibility you need to log progress, halt execution, perform concurrent evaluations, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "| \u001b[39m3        \u001b[39m | \u001b[39m-18.41   \u001b[39m | \u001b[39m1.9506186\u001b[39m | \u001b[39m-2.950721\u001b[39m |\n",
      "-18.413111112960056 {'x': np.float64(1.9506186451101901), 'y': np.float64(-2.9507212017944955)}\n",
      "| \u001b[39m4        \u001b[39m | \u001b[39m0.7603   \u001b[39m | \u001b[39m-0.379805\u001b[39m | \u001b[39m1.3089202\u001b[39m |\n",
      "0.7603162209132889 {'x': np.float64(-0.37980530851809036), 'y': np.float64(1.3089202270946163)}\n",
      "| \u001b[39m5        \u001b[39m | \u001b[39m-6.841   \u001b[39m | \u001b[39m-1.990473\u001b[39m | \u001b[39m2.9694974\u001b[39m |\n",
      "-6.840906127161674 {'x': np.float64(-1.9904737772920469), 'y': np.float64(2.9694974661254085)}\n",
      "| \u001b[39m6        \u001b[39m | \u001b[39m-6.879   \u001b[39m | \u001b[39m1.9740210\u001b[39m | \u001b[39m2.9954409\u001b[39m |\n",
      "-6.8785435274794136 {'x': np.float64(1.9740210595375953), 'y': np.float64(2.995440899646362)}\n",
      "| \u001b[39m7        \u001b[39m | \u001b[39m-7.124   \u001b[39m | \u001b[39m-1.985509\u001b[39m | \u001b[39m-1.044851\u001b[39m |\n",
      "-7.123667302755344 {'x': np.float64(-1.9855094780813816), 'y': np.float64(-1.0448519298972099)}\n",
      "{'target': np.float64(0.7861845912690544), 'params': {'x': np.float64(-0.331911981189704), 'y': np.float64(1.3219469606529486)}}\n"
     ]
    }
   ],
   "source": [
    "for _ in range(5):\n",
    "    next_point = optimizer.suggest()\n",
    "    target = black_box_function(**next_point)\n",
    "    optimizer.register(params=next_point, target=target)\n",
    "\n",
    "    print(target, next_point)\n",
    "print(optimizer.max)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Tuning the underlying Gaussian Process\n",
    "\n",
    "The bayesian optimization algorithm works by performing a gaussian process regression of the observed combination of parameters and their associated target values. The predicted parameter $\\rightarrow$ target hyper-surface (and its uncertainty) is then used to guide the next best point to probe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1 Passing parameter to the GP\n",
    "\n",
    "Depending on the problem it could be beneficial to change the default parameters of the underlying GP. You can use the `optimizer.set_gp_params` method to do this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "|   iter    |  target   |     x     |     y     |\n",
      "-------------------------------------------------\n",
      "| \u001b[39m2        \u001b[39m | \u001b[39m0.7862   \u001b[39m | \u001b[39m-0.331911\u001b[39m | \u001b[39m1.3219469\u001b[39m |\n",
      "| \u001b[39m3        \u001b[39m | \u001b[39m-18.34   \u001b[39m | \u001b[39m1.9021640\u001b[39m | \u001b[39m-2.965222\u001b[39m |\n",
      "| \u001b[35m4        \u001b[39m | \u001b[35m0.8731   \u001b[39m | \u001b[35m-0.298167\u001b[39m | \u001b[35m1.1948749\u001b[39m |\n",
      "| \u001b[39m5        \u001b[39m | \u001b[39m-6.497   \u001b[39m | \u001b[39m1.9876938\u001b[39m | \u001b[39m2.8830942\u001b[39m |\n",
      "| \u001b[39m6        \u001b[39m | \u001b[39m-4.286   \u001b[39m | \u001b[39m-1.995643\u001b[39m | \u001b[39m-0.141769\u001b[39m |\n",
      "| \u001b[39m7        \u001b[39m | \u001b[39m-6.781   \u001b[39m | \u001b[39m-1.953302\u001b[39m | \u001b[39m2.9913127\u001b[39m |\n",
      "=================================================\n"
     ]
    }
   ],
   "source": [
    "optimizer = BayesianOptimization(\n",
    "    f=black_box_function,\n",
    "    pbounds={'x': (-2, 2), 'y': (-3, 3)},\n",
    "    verbose=2,\n",
    "    random_state=1,\n",
    ")\n",
    "optimizer.set_gp_params(alpha=1e-3, n_restarts_optimizer=5)\n",
    "optimizer.maximize(\n",
    "    init_points=1,\n",
    "    n_iter=5\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 Tuning the `alpha` parameter\n",
    "\n",
    "When dealing with functions with discrete parameters,or particularly erratic target space it might be beneficial to increase the value of the `alpha` parameter. This parameters controls how much noise the GP can handle, so increase it whenever you think that extra flexibility is needed."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.3 Changing kernels\n",
    "\n",
    "By default this package uses the Matern 2.5 kernel. Depending on your use case you may find that tuning the GP kernel could be beneficial. You're on your own here since these are very specific solutions to very specific problems. You should start with the [scikit learn docs](https://scikit-learn.org/stable/modules/gaussian_process.html#kernels-for-gaussian-processes)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "bayesian-optimization-t6LLJ9me-py3.10",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.1"
  },
  "nbdime-conflicts": {
   "local_diff": [
    {
     "diff": [
      {
       "diff": [
        {
         "key": 0,
         "op": "addrange",
         "valuelist": [
          "3.1.0"
         ]
        },
        {
         "key": 0,
         "length": 1,
         "op": "removerange"
        }
       ],
       "key": "version",
       "op": "patch"
      }
     ],
     "key": "language_info",
     "op": "patch"
    }
   ],
   "remote_diff": [
    {
     "diff": [
      {
       "diff": [
        {
         "key": 0,
         "op": "addrange",
         "valuelist": [
          "3.10.12"
         ]
        },
        {
         "key": 0,
         "length": 1,
         "op": "removerange"
        }
       ],
       "key": "version",
       "op": "patch"
      }
     ],
     "key": "language_info",
     "op": "patch"
    }
   ]
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
